팀프로젝트/SpringBoot

스프링부트 팀플) 20240414 JWT 적용(Oauth2 포함)

일일일코_장민기 2024. 4. 14. 14:04
728x90

JWT란? ( JSON Web Token)

- Map의 데이터타입처럼 Key-Value 구조를 가지고 있으며 Web Token으로 사용할 수 있다.

 

왜 토큰을 쓸까?

--> 이걸 이해하기 위해서는 쿠키와 세션에 대해 알아야 한다.

 

그러면 쿠키와 세션을 왜 쓸까?

- HTTP 통신은 Request와 Response가 끝나면 stateless이기 때문에 연결을 끊는다. (쉽게 말해 요청-> 종료 과정이 종료되면 그 상태가 더이상 유지되지 않는다)

 

--> 이런 특징으로 인해 로그인 상태 유지에 문제가 발생한다.

 

---> 누가 로그인 중인지 기억하기 위해 쿠키/세션/토큰을 사용한다.

 

쿠키란?

- 공개 가능한 정보를 사용자의 브라우저에 저장시켜, 사용자의 사용 경험을 향상시켜준다.

- 서버는 클라이언트 측에 저장하고 싶은 정보를 응답 헤더의 set-cookies에 담는다.

- 클라이언트가 재요청할 때마다 저장된 쿠키를 요청 헤더의 cookie에 담아서 전송한다.

==> 서버는 전송된 쿠키를 통해 해당 요청의 클라이언트를 식별한다.

 

세션이란?

- 공개하면 안 되는 정보를 사용자의 브라우저에 저장해서는 안 되기 때문에 서버에서 저장할 필요가 있다.

- 서버는 비밀번호와 같은 인증정보와 사용자의 식별자인 session id를 저장한다.

***문제: 사용자가 늘어날 수록 해당 유저의 정보를 찾고, 데이터 매칭을 하는데 시간이 장기화되고 서버에 부담이 된다.

 

토큰이란?

- 인가의 목적을 위해 사용되는 이용권한/자격을 나타내는 징표

- 인증/권한 정보를 담고 있는 암호화된 문자열

--> 이를 통해 특정 어플리케이션을 이용하는 유저의 권한 정보를 파악하여 인가/비인가 판정을 내릴 수 있다.

==> 서버에서 유저의 인증상태를 저장하지 않고, 클라이언트에 저장하여 세션에서 문제가 되었던 서버 과부하/메모리 부족 문제를 줄일 수 있다.

 

1. 유저가 인증정보를 담아 서버에 로그인 요청을 보냅니다.
2. 서버는 데이터베이스에 저장된 유저의 인증정보를 확인합니다.
3. 인증에 성공했다면, 서버는 유저에 대한 권한정보를 서버의 비밀키와 함께 토큰을 생성합니다.
4. 서버는 Authorization 헤더에 토큰을 담아 클라이언트에 전달합니다.
5. 클라이언트는 전달받은 토큰을 브라우저의 세션 스토리지 or 로컬스토리지 에 저장합니다.
6. 클라이언트가 서버로 리소스를 요청할때, Authorization 헤더를 통해 토큰이 함께 전달됩니다.
7. 서버는 전달받은 토큰을 서버의 비밀키로 검증합니다 이를 통해, 토큰이 위조되었는지 토큰의 유효기간이 지나지 않았는지 등을 확인할 수 있습니다.
8. 토큰이 유효하다면, 유저의 요청에 대한 응답 데이터를 전송합니다.

 

 

-- 세션/쿠키 vs 토큰을 비교한 이미지 --

 

 

 

그러면 토큰의 장점은 무엇일까?

1. 무상태성

- 서버에서는 유저의 상태를 관리하지 않고, 오직 토큰의 유효성만 검증한다

--> 유저의 상태를 관리하는 로직이 필요없는 무상태적인 아키텍처를 구축할 수 있다

(개발자가 데이터 일관성과 일관성을 보장하기 위해, 여러 서버 인스턴스 간에 상태 저장 데이터를 동기화하는 것에 대해 걱정할 필요가 없으므로 전체 시스템 설계를 단순화하는 데 도움이 된다)

 

2. 확장성

- 하나의 토큰으로 다수의 서버에 인증 가능하다

--> 다수의 서버에 공통의 세션 데이터를 가질 필요가 없다(서버의 부담 감소)

 

3. 어디서나 토큰 생성 가능

- 토큰 생성만을 담당하는 인증용 서버를 통해, 여러 앱을 하나의 토큰으로 인증하는 등의 활용이 가능하다

--> 여러 앱마다 각각의 인증 과정을 만들 필요가 없어진다(시스템 설계 투자 리소스 감소)

 

4. 권한 부여에 용이

- 사용자의 인증정보 + 권한 정보까지 담아서 암호화

--> 기존의 세션이 하던 역할과 크게 다르지 않다(그러면 세션의 문제점을 개선하는 토큰을 쓰는 것이 낫지 않을까?)

 

토큰의 단점

- 서버 측에서 사용자 인증정보를 관리하는 세션과 달리, 클라이언트 측에서 관리하여 보안 문제가 발생하기 쉽다

--> 탈취 우려(탈취되면 탈취자가 탈취된 유저의 행세를 하는 것이 가능해진다)

==> 추가적인 보안 처리가 필요하다

 

- 토큰 만료 시간 설정 문제

--> 세션과 달리 만료시간 설정이 어렵고, 만료시간이 지나면 다시 로그인해야 한다.

 

단점의 커버 방법

- HttpOnly를 통해 JS코드로 접근할 수 없도록 설정

- HTTPS 사용을 통해 통신을 암호화하여 중간자 공격 등으로부터 보호

- 토큰의 암호화(JWT가 기본적으로 해준다)

- access토큰의 만료기간을 짧게 하고 refresh토큰을 통해 갱신하도록 처리(이건 다음 포스팅에서 진행)

 

JWT은 이러한 토큰의 특징을 가지면서 자동으로 암호화까지 진행할 수 있는 양방향 암호화이다.

참고로 구글에서 access Token을 받아보면 JWT로 암호화된 것을 받을 수 있다.

 

아무튼 이러한 장점을 가진 JWT를 학습과 이후 업무에 적용시킬  수 있도록 되기 위해 프로젝트에 적용하기로 했다.

(사실 아카데미 프로젝트 같은 소규모 프로젝트에서는 학습 목적 외에 사용할 이유는 없다고 생각한다.)

 

package com.moonBam.springSecurity;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.moonBam.springSecurity.JWT.JWTFilter;
import com.moonBam.springSecurity.JWT.JWTUtil;
import com.moonBam.springSecurity.JWT.LoginFilter;
import com.moonBam.springSecurity.JWT.LoginSuccessHandler;
import com.moonBam.springSecurity.oauth2.OAuthService;


@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
public class SecurityConfig { // WebSecurityConfigurerAdapter는 securityFilterChain과 동시 사용 불가

	private final OAuthService oAuthService;

	private final AuthenticationConfiguration authenticationConfiguration;
	
	private final JWTUtil jwtUtil;
	
	private final LoginSuccessHandler loginSuccessHandler;

	public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, OAuthService oAuthService, JWTUtil jwtUtil, LoginSuccessHandler loginSuccessHandler) {
	    this.authenticationConfiguration = authenticationConfiguration;
	    this.oAuthService = oAuthService;
	    this.jwtUtil = jwtUtil;
	    this.loginSuccessHandler = loginSuccessHandler;
	}

	//AuthenticationManager Bean 등록
	@Bean
		public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
		return configuration.getAuthenticationManager();
	}
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}
	
   @Bean
   public RoleHierarchy roleHierarchy() {						//계층형 권한 부여
	   RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
	   	hierarchy.setHierarchy("ROLE_ADMIN > ROLE_MEMBER");		//ADMIN:	MEMBER의 모든 권한 + ADMIN 고유의 권한
	   return hierarchy;
   }
		
	@Bean  //configure는 Override의 기본값(SecurityFilterChain를 사용할 때는 메서드 명칭 변경)
	public SecurityFilterChain securityFilterChain(HttpSecurity security) throws Exception {
		
		security.httpBasic().disable();	//사용자가 서비스에 접근할 때 사용자 이름과 비밀번호를 HTTP 요청 헤더에 인코딩하여 보내는 간단한 인증 방식
										//보안에 취약하며, 사용자 이름과 비밀번호를 암호화하지 않기 때문에 안전하지 않음
		
		security.csrf().disable();		//csrf:	요청을 위조하여 사용자가 원하지 않아도 서버 측으로 특정 요청을 강제로 보내는 방식
										//		회원 정보 변경 / 게시글 CRUD를 사용자 모르게 요청
		
		//로그인 설정
		security.formLogin().disable();
		security.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
		security.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

		//소셜 로그인 설정
//		security.oauth2Login(Customizer.withDefaults());
		
		security.oauth2Login(oauth2 -> oauth2
				.loginPage("/mainLogin")
				.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig.userService(oAuthService))
				.successHandler(loginSuccessHandler)
				);

		//권한따라 허용되는 url 설정
				security.authorizeHttpRequests().antMatchers("/").permitAll();
				security.authorizeHttpRequests().antMatchers("/memberList").hasRole("ADMIN");		//ROLE은 무조건 // ADMIN은 관례
//				security.authorizeHttpRequests().anyRequest().permitAll();
		
		//세션관리
		security.sessionManagement()
				.maximumSessions(1)											//	최대 동시 접속 1개
				.maxSessionsPreventsLogin(true);							//	True:	동시 접속 시 새로운 로그인 차단
		
		security.sessionManagement().sessionFixation().changeSessionId();	//로그인 시 동일한 세션에 대한 id 변경
		
		security.sessionManagement((session) -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS));	//JWT를 통한 인증/인가를 위해서 세션을 STATELESS 상태로 설정
		
		//로그인 실패
		security.exceptionHandling().accessDeniedPage("/NotAuthentic");		//권한 없으면 가는 페이지

		//로그아웃
		security.logout().logoutUrl("/Logout").logoutSuccessUrl("/").invalidateHttpSession(true);
		
		return security.build();
	}
	
    public void configure(WebSecurity web) throws Exception {
    		web.ignoring().antMatchers("/static/**");
    }
}

private final OAuthService oAuthService; //각 소셜 로그인별로 데이터를 뽑아오기 위한 서비스

 

private final AuthenticationConfiguration authenticationConfiguration;

// 사용자의 인증을 처리하는 중요한 인터페이스를 위한 사용자 지정 구성을 위한 클래스

 

private final JWTUtil jwtUtil;

//JWT 파싱 장치

 

private final LoginSuccessHandler loginSuccessHandler;

//로그인 성공 시 작동(OAuth2 + JWT에서 사용)

 

public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, OAuthService oAuthService, JWTUtil jwtUtil, LoginSuccessHandler loginSuccessHandler) {

this.authenticationConfiguration = authenticationConfiguration;

this.oAuthService = oAuthService;

this.jwtUtil = jwtUtil;

this.loginSuccessHandler = loginSuccessHandler;

}

 

//로그인 설정

security.formLogin().disable();

security.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

security.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class);

- 기존의 formLogin 방식이 아닌, JWT기반의 인증 로그인 방식으로 바꿈

- JWTFilter -> LoginFilter 순서로 진행

 

//소셜 로그인 설정

security.oauth2Login(oauth2 -> oauth2

.loginPage("/mainLogin")

.userInfoEndpoint((userInfoEndpointConfig) -> userInfoEndpointConfig.userService(oAuthService))

.successHandler(loginSuccessHandler)

);

- 제대로 작동되지 않으면 mainLogin로 이동된다

- 소셜네트워크 사이트에서 정보를 받으면 oAuthService로 정보가 이동되며, 해당 처리가 끝나면 loginSuccessHandler가 작동된다.

 

security.sessionManagement((session) -> session

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); //JWT를 통한 인증/인가를 위해서 세션을 STATELESS 상태로 설정

- 설명은 주석 참조

 

 

 

 

package com.moonBam.springSecurity.JWT;

import java.security.Key;
import java.util.Date;

import javax.crypto.SecretKey;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureAlgorithm;

@Component
public class JWTUtil {

    private SecretKey key;

    public JWTUtil(@Value("${spring.jwt.secret}")String secret) {
		byte[] byteSecretKey = Decoders.BASE64.decode(secret);
        key = Keys.hmacShaKeyFor(byteSecretKey);
    }

    public String getUsername(String token) {
//12.3  return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().get("username", String.class);
        return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload().get("username", String.class);
    }

    public String getRole(String token) {
//12.3  return Jwts.parser().setSigningKey(key).build().parseClaimsJws(token).getBody().get("role", String.class);
        return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload().get("role", String.class);
    }

    public Boolean isExpired(String token) {
//12.3  return Jwts.parser().setSigningKey(key).build().parseClaimsJws(token).getBody().getExpiration().before(new Date());
        return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
    }

    public String createJwt(String username, String role, Long expiredMs) {
    	System.out.println(username + " : " + role + " : " + expiredMs);
    	return Jwts.builder()
                .claim("username", username)
                .claim("role", role)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + expiredMs))
                .signWith(key)
                .compact();
    }
}

 

- 적용버전은 12.5이다. (11.5, 12.3과도 다르니 주의)

- JWT를 받으면 그 JWT를 분해해서 필요하는 것을 추출하는 작업 // 필요한 것만 모아서 JWT로 만들어주는 작업을 한다.

- spring.jwt.secret의 경우, application.properties에 만드는 임의의 32글자 이상의 값이다

(32글자 이상인 이유는 #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 하기 때문이다.)

 

 

package com.moonBam.springSecurity.JWT;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.security.core.Authentication;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

import com.moonBam.dto.MemberDTO;
import com.moonBam.springSecurity.SpringSecurityUser;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.io.IOException;

public class JWTFilter extends OncePerRequestFilter {

    private final JWTUtil jwtUtil;

    public JWTFilter(JWTUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, java.io.IOException {

    	//재로그인 무한 루프 오류 방지
    	//JWT가 만료된 상태에서 재로그인되면 OAuth2 로그인 실패 --> 재요청 --> 무한루프 발생
    	String requestUri = request.getRequestURI();
    	if (requestUri.matches("^\\/login(?:\\/.*)?$")) {
    	    filterChain.doFilter(request, response);
    	    return;
    	}
    	if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {
    	    filterChain.doFilter(request, response);
    	    return;
    	}
    	
		//쿠키에 user 식별 key 있는지 확인
    	String JWTtoken = "";
    	Cookie[] cookies = request.getCookies();
    	System.out.println("cookies: " + cookies);
    	if (cookies != null) {
    	    for (Cookie cookie : cookies) {
    	        System.out.println("cookie: " + cookie.getName());
    	        if (cookie.getName().equals("JWTtoken")) {
    	            JWTtoken = cookie.getValue();
    	            System.out.println("JWTtoken: " + JWTtoken);
    	            break; // 찾았으면 루프 종료
    	        }
    	    }
    	}
    	System.out.println("JWTtoken: " + JWTtoken);
		
	    // 사용자 식별 키가 없으면 다음 필터로 이동
	    if (JWTtoken.isEmpty()) {
	        filterChain.doFilter(request, response);
	        return;
	    }
        
	    String token = (String) JWTtoken;
        System.out.println("JWTFilter: "+token);
    	
        try {
            //토큰 소멸 시간 검증
        	jwtUtil.isExpired(token);
        } catch (ExpiredJwtException e) {
            // JWT가 만료된 경우 처리
        	System.out.println("token is expired!!!!!!!!!!!!!!!!!!!!");
            filterChain.doFilter(request, response);
            return;
        }

		//토큰에서 username과 role 획득
        String username = jwtUtil.getUsername(token);
        String role = jwtUtil.getRole(token);
				
		//MemberDTO를 생성하여 값 set
        MemberDTO dto = new MemberDTO();
	        dto.setUserId(username);
	        dto.setUserPw("temppassword");
	        dto.setRole(role);
//	        System.out.println(dto);
				
		//SpringSecurityUser에 회원 정보 객체 담기
	    SpringSecurityUser customUserDetails = new SpringSecurityUser(dto);

		//스프링 시큐리티 인증 토큰 생성
        Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
		System.out.println("JTWFilter: "+authToken.getName());
        
        //세션에 사용자 등록
        SecurityContextHolder.getContext().setAuthentication(authToken);
        filterChain.doFilter(request, response);
    }
}

 

참고로 JWT토큰 같은 경우는 어떤 동작을 하든지 계속 불러온다.

평범한 사이트 내 이동을 할 때마다 이렇게 호출이 많이 된다.

 

//재로그인 무한 루프 오류 방지

//JWT가 만료된 상태에서 재로그인되면 OAuth2 로그인 실패 --> 재요청 --> 무한루프 발생

String requestUri = request.getRequestURI();

if (requestUri.matches("^\\/login(?:\\/.*)?$")) {

filterChain.doFilter(request, response);

return;

}

if (requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {

filterChain.doFilter(request, response);

return;

}

- 이런 오류가 날 수 있다고 들어서 추가해두었다.

- 사용 후 추후 재기재 예정

 

try {

//토큰 소멸 시간 검증

jwtUtil.isExpired(token);

} catch (ExpiredJwtException e) {

// JWT가 만료된 경우 처리

System.out.println("token is expired!!!!!!!!!!!!!!!!!!!!");

filterChain.doFilter(request, response);

return;

}

- 이 처리의 경우, token을 받았을 때 parser에서 exception이 터질 수 있기 때문에 이렇게 처리했다.

- 에러 확인 코드를 입력해보면 token이 없을 때마다 에러가 터지는 것을 볼 수 있다.

- 그 외에는 주석 참조

 

package com.moonBam.springSecurity.JWT;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.moonBam.springSecurity.SpringSecurityUser;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;
    
    private final JWTUtil jwtUtil;

    public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {
        this.authenticationManager = authenticationManager;
		this.jwtUtil = jwtUtil;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

		//클라이언트 요청에서 username, password 추출
    	// String username = obtainUsername(request);
    	// String password = obtainPassword(request);
    	String username = request.getParameter("userId");
    	String password = request.getParameter("userPw");
        System.out.printf("Username: "+ username+" Password: "+ password);

		//스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
        
		//token에 담은 검증을 위한 AuthenticationManager로 전달
        return authenticationManager.authenticate(authToken);
    }

	//로그인 성공시 실행하는 메소드 (여기서 JWT 발급)
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
				
		//인증객체에서 사용자 정보 추출(springSecurityUser - 아이디 / 패스워드 / 역할 / 활성화)
    	SpringSecurityUser springSecurityUser = (SpringSecurityUser) authentication.getPrincipal();

    	//사용자 정보 중 아이디 추출
        String username = springSecurityUser.getUsername();

        //사용자 정보 중 사용자 권한 목록 추출
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        //사용자 권한 목록을 순회하는 iterator
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();

        //iterator 중 첫번째 권한을 auth에 저장
        GrantedAuthority auth = iterator.next();

        //role에 권한 객체에서 실제 권한을 추출한 것을 저장 
        String role = auth.getAuthority();

        //아이디와 역할을 통해 JWT토큰 생성(10시간 유지)
        String token = jwtUtil.createJwt(username, role, 60*60*12000L);
        System.out.println("LoginFilter token: "+ token);
        
        Cookie cookie = new Cookie("JWTtoken", token);
        	cookie.setMaxAge(60*60*12);
        	response.addCookie(cookie);
        	
        // 루트 주소로 리다이렉트
        response.sendRedirect("logining/");
    }

	//로그인 실패시 실행하는 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {

    	//로그인 실패시 401 응답 코드 반환
        response.setStatus(401);
    }
}

 

- 주석 참조

 

 

 

OAuth2 + JWT

- SecurityConfig 설정을 위를 참조

 

package com.moonBam.springSecurity.JWT;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.moonBam.springSecurity.SpringSecurityUser;
import com.moonBam.springSecurity.oauth2.OAuthUser;

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

	private final JWTUtil jwtUtil;

    public LoginSuccessHandler(JWTUtil jwtUtil) {

        this.jwtUtil = jwtUtil;
    }
	
	@Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

		//인증객체에서 사용자 정보 추출(springSecurityUser - 아이디 / 패스워드 / 역할 / 활성화)
		OAuthUser oAuthUser = (OAuthUser) authentication.getPrincipal();

    	//사용자 정보 중 아이디 추출
        String username = oAuthUser.getUsername();

        //사용자 정보 중 사용자 권한 목록 추출
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

        //사용자 권한 목록을 순회하는 iterator
        Iterator<? extends GrantedAuthority> iterator = authorities.iterator();

        //iterator 중 첫번째 권한을 auth에 저장
        GrantedAuthority auth = iterator.next();

        //role에 권한 객체에서 실제 권한을 추출한 것을 저장 
        String role = auth.getAuthority();

        //아이디와 역할을 통해 JWT토큰 생성(10시간 유지)
        String token = jwtUtil.createJwt(username, role, 60*60*12000L);
        System.out.println("LoginSuccessHandler token: "+ token);
        
        Cookie cookie = new Cookie("JWTtoken", token);
        	cookie.setMaxAge(60*60*12);
        	cookie.setPath("/acorn");
        	response.addCookie(cookie);
		
        response.sendRedirect("/acorn/logining");	
    }
}

 

- 위에 서술한 LoginFilter와 거의 유사하다. 인터페이스에 따른 데이터 객체만 달라질 뿐

- 다만 sendRedirect를 쓸 때 callBack주소에서 오는 것이기 때문에 주소처리를 잘하지 않으면 엉뚱한 곳으로 간다.

 

 

@GetMapping("/logining")

public String logining(Principal principal, HttpSession session) {

 

System.out.println("principal: "+principal);

 

// principal이 null인지 확인

if (principal == null) {

// 로그인하지 않은 사용자가 접근한 경우에 대한 처리

return "redirect:/";

}

 

String userId = principal.getName();

System.out.println("userId: "+userId);

 

//닉네임 찾기

MemberDTO memberData = dao.userDetail(userId);

MemberDTO dto = new MemberDTO();

dto.setUserId(userId);

dto.setNickname(memberData.getNickname());

dto.setRole(memberData.getRole());

dto.setEnabled(memberData.isEnabled());

System.out.println("dto: "+dto);

session.setAttribute("loginUser", dto);

return "redirect:/";

}

- jsp에서 현재 우리 프로젝트에 맞게 session 설정

 

 

 

 

이것으로 기본적인 일반 로그인+JWT / 소셜 로그인 + JWT를 만들었다.

 

개선사항

1. 현재 프로젝트 전반의 세션 처리를 pricipal 처리를 통해 불러오는 형식으로 바꿀 것

2. 동일 주소를 여러 번 클릭하면 세션이 사라지는 문제

3. Acess Token의 유효기간을 짧게 하고, Refresh Token을 통해 갱신