본문 바로가기
Back end/Spring

[Spring] 회원가입, 로그인, 요청 인증(JWT 토큰)

by 더 이프 2023. 8. 17.
728x90

목차

    1. 기본 설정

    ■ Securityconfig

    a. 보안 설정

    • WebSecurityConfigurerAdapter를 상속 받아서 사용자 정의 보안 구성을 작성 ➡ 보안 설정을 하는 것
    • @EnalbeWebSecurity 어노테이션을 사용해서 Spring Security를 활성화
    • @Configuration 어노테이션을 적용하여 모든 요청을 거칠 때 해당 클래스를 거치게 됨
    • BCrypt를 통해 암호화 한 것을 해석할 수 있는 passwordEncoder() 메소드를 @Bean 어노테이션을 붙여서 IOC에 등록
      • BCryptPasswordEncoder는 라이브러리에 등록되어 있어 @Component를 적용할 수 없기 때문에 config에서 적용
    • configure 메소드는 HTTP 요청에 대한 보안 구성을 정의
    • http.csrf().disable() 을 사용해서 CSRF 보호 기능을 비활성화
    • authorizeRequests()를 사용해서 요청에 대한 인증 및 권한 구성
    • antMatchers에 로그인과 회원가입 요청uri를 넣어서 해당 요청들은 인증되지 않아도 접근 가능
    • anyRequest().authenticated()를 사용해서 모든 요청에 인증이 되어야만 요청이 가능

    b. JWT 토큰 요청 설정

    • JWT를 통한 사용자 정의 인증을 위해 HttpBasic의 웹 기본 인증방식과 formLogin의 폼태그를 통한 로그인 방식을 비활성화
    • 해당 코드는 session을 사용하지 않기 때문에 세션 비활성화
    • /courses에 관한 요청들은 ROLE_ADMIN 권한이 잇는 사용자만 요청을 보낼 수 있음
      • ROLE_ 접두어는 hasRole메소드를 통해 자동으로 적용
    • 나머지 모든 요청들은 authenticate에 사용자 정보가 있어야 인증됨
    • addFilterBefore()를 통해 해당 필터를 거치기 전에 JwtAuthenticationFilter(사용자 인증 필터)를 거치도록 함
      • 사용자 인증 필터에서 JWT 토큰이 유효한지 테스트
      • 에러 발생시 JwtAuthenticationEntryPoint로 이동
    package com.web.study.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import com.web.study.security.jwt.JwtAuthenticationEntryPoint;
    import com.web.study.security.jwt.JwtAuthenticationFilter;
    import com.web.study.security.jwt.JwtTokenProvider;
    
    import lombok.RequiredArgsConstructor;
    
    @EnableWebSecurity
    @Configuration
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
    	
    	private final JwtTokenProvider jwtTokenProvider;
    	private final JwtAuthenticationEntryPoint authenticationEntryPoint;
    	
    	@Bean
    	public BCryptPasswordEncoder passwordEncoder() {
    		return new BCryptPasswordEncoder();
    	}
    
    	// security filter
    	// controller 거치지 전에 무조건 적용
    	@Override
    	protected void configure(HttpSecurity http) throws Exception {
    		http.csrf().disable();
    		http.httpBasic().disable(); // 웹 기본 인증 방식 
    		http.formLogin().disable(); // 폼태그를 통한 로그인
    		http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션 비활성
    		http.authorizeRequests()
    			.antMatchers("/auth/register/**","/auth/login/**")
    			.permitAll()
    			.antMatchers("/courses")
    			.hasRole("ADMIN")
    			.anyRequest()
    			.authenticated()
    			.and()
    			.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
    			.exceptionHandling()
    			.authenticationEntryPoint(authenticationEntryPoint);
    	}
    }

     

    2. 회원가입

    • 회원가입할 정보를 입력
    • 해당 정보로 회원가입 Post 요청
    • AuthController에서 요청 정보를 받아옴
    • username 중복 확인(중복 시 advice로 가서 예외 처리)
    • AuthService에서 Dto를 Entity로 변환할 때 password 암호화
    • UserRepository에서 DB에 접근하여 회원가입 정보를 DB에 저장

    ■ RegisteUserReqDto

    • 회원가입 요청을 받을 때 요청받을 정보(username, password, name, email)를 멤버 변수로 받음
    • AuthService에서 Dto를 Entity로 변환할 메소드를 생성하고 password는 암호화하여 전달
    package com.web.study.dto.auth;
    
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    import com.web.study.domain.entity.User;
    
    import lombok.Data;
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @Data
    @ToString
    public class RegisteUserReqDto {
    	private String username;
    	private String password;
    	private String name;
    	private String email;
    	
    	public User toEntity() {
    		return User.builder()
    				.username(username)
    				.password(new BCryptPasswordEncoder().encode(password))
    				.name(name)
    				.email(email)
    				.build();
    	}
    }

    ■ AuthController

    • /auth/register로 Post 요청을 보내 회원가입 진행
    • authService에서 duplicatedUsername을 해서 아이디 중복체크 진행
    • 아이디 중복확인 후 registerUser가 실행되서 user 객체 생성
    package com.web.study.controller.auth;
    
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.web.study.dto.DataResponseDto;
    import com.web.study.dto.ResponseDto;
    import com.web.study.dto.auth.LoginReqDto;
    import com.web.study.dto.auth.RegisteUserReqDto;
    import com.web.study.service.AuthService;
    
    import lombok.RequiredArgsConstructor;
    
    @RestController
    @RequiredArgsConstructor
    public class AuthController {
    	private final AuthService authService;
    	
    	@PostMapping("/auth/register")
    	public ResponseEntity<? extends ResponseDto> registe(@RequestBody RegisteUserReqDto registeUserReqDto) {
    		authService.duplicatedUsername(registeUserReqDto);
    		authService.registeUser(registeUserReqDto);
    		return ResponseEntity.ok().body(ResponseDto.ofDefault());
    	}
    	
    	@PostMapping("/auth/login")
    	public ResponseEntity<? extends ResponseDto> login(@RequestBody LoginReqDto loginReqDto) {
    		return ResponseEntity.ok().body(DataResponseDto.of(authService.login(loginReqDto)));
    	}
    }

    ■ AuthService

    • Dto에서 값을 받아와서 중복체크와 회원가입 실행
    package com.web.study.service;
    
    import com.web.study.dto.auth.LoginReqDto;
    import com.web.study.dto.auth.RegisteUserReqDto;
    import com.web.study.dto.response.auth.JwtTokenRespDto;
    
    public interface AuthService {
    	public void registeUser(RegisteUserReqDto registeUserReqDto);
    	public void duplicatedUsername(RegisteUserReqDto registeUserReqDto);
    	
    	public JwtTokenRespDto login(LoginReqDto loginReqDto);
    }

    ■ AuthServiceImpl

    • duplicatedUsername 메소드는 중복되는 아이디가 있는지 확인하는 메소드
      • userRepository에서 findUserByUsername을 사용해서 중복되는 username이 있으면 error 반환
      • 중복되는 username이 없으면 계속 진행
    • registeUser 메소드는 회원가입을 진행하는 유저 정보를 등록해주는 메소드
      • userRepository에서 saveUser를 사용해서 유저 등록
      • authority 정보를 저장할 수 있는 리스트 생성
      • 생성된 user_id를 받아와서 해당 user_id에  1번(ROLE_USER) 권한을 부여한 authority를 하나 생성하여 리스트에 추가
      • addAuthorities에 해당 리스트를 넣어서 권한을 추가
    • user를 추가한 뒤 추가한 user에 ROLE_USER 권한을 부여하는 코드를 작성한 예시
    package com.web.study.service;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.core.Authentication;
    import org.springframework.stereotype.Service;
    
    import com.web.study.domain.entity.Authority;
    import com.web.study.domain.entity.User;
    import com.web.study.dto.auth.LoginReqDto;
    import com.web.study.dto.auth.RegisteUserReqDto;
    import com.web.study.dto.response.auth.JwtTokenRespDto;
    import com.web.study.exception.CustomException;
    import com.web.study.repository.UserRepository;
    import com.web.study.security.jwt.JwtTokenProvider;
    
    import lombok.RequiredArgsConstructor;
    
    @Service
    @RequiredArgsConstructor
    public class AuthServiceImpl implements AuthService{
    	private final UserRepository userRepository;
    	private final AuthenticationManagerBuilder authenticationManagerBuilder;
    	private final JwtTokenProvider jwtTokenProvider;
    	@Override
    	public void registeUser(RegisteUserReqDto registeUserReqDto) {
    		User userEntity = registeUserReqDto.toEntity();
    		
    		userRepository.saveUser(userEntity);
    		
    		List<Authority> authorities = new ArrayList<>();
    		authorities.add(Authority.builder().user_id(userEntity.getUser_id()).role_id(1).build());
    		
    		userRepository.addAuthorities(authorities);
    	}
    
    	@Override
    	public void duplicatedUsername(RegisteUserReqDto registeUserReqDto) {
    		User userEntity = userRepository.findUserByUsername(registeUserReqDto.getUsername());
    		
    		if(userEntity != null) {
    			Map<String, String> errorMap = new HashMap<>();
    			errorMap.put("username", "이미 사용중인 사용자이름입니다.");
    			
    			throw new CustomException("중복 검사 오류", errorMap);
    		}
    	}
    
    	@Override
    	public JwtTokenRespDto login(LoginReqDto loginReqDto) {
    		
    		// security가 알아볼수 있는 형태로 username과 password를 변환(매니저가 알아먹을수 있는 형태)
    		UsernamePasswordAuthenticationToken authenticationToken = 
    				new UsernamePasswordAuthenticationToken(loginReqDto.getUsername(), loginReqDto.getPassword());
    		
    		// 인증이 완료가 되면 authentication 객체를 생성
    		// 실제 로그인 정보를 authentication안에서 관리
    		// UserDetailsService의 loadUserByUsername()이 호출이 된다!!!
    		// authentication에 Principal로 업 캐스팅 되어 적용
    		Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    		return jwtTokenProvider.createToken(authentication);
    	}
    }

    ■ UserRepository

    • saveUser는 회원가입 유저 추가 메소드
    • addAuthorities는 권한 부여 메소드
    • findUserByUsername은 유저 정보 중복 확인 메소드
    package com.web.study.repository;
    
    import java.util.List;
    
    import org.apache.ibatis.annotations.Mapper;
    
    import com.web.study.domain.entity.Authority;
    import com.web.study.domain.entity.User;
    
    @Mapper
    public interface UserRepository {
    	public int saveUser(User user);
    	public int addAuthorities(List<Authority> authorities);
    	public User findUserByUsername(String username);
    }

    ■ User.xml

    • Role, Authority, User의 resultMap 생성
    • saveUser에서 매개변수로 받은 값들을 insert
      • 데이터베이스에서 auto increment로 생성된 user_id를 받아오기 위해 useGeneratedKeys를 true로 적용
      • KeyProperty에 user_id를 넣어줘서 user_id에 키값을 넣어줌
      • 해당 키워드를 통해 service에서 권한을 추가할 때 해당 user_id에 권한 추가 가능
    • addAuthorities에서는 권한 부여 리스트를 받아와서 insert
      • foreach를 사용하여 매개변수로 받은 리스트를 반복을 돌려 꺼내 separator인 쉼표로 구분하여 순서대로 나열
      • 하나씩 꺼내진 권한들을 데이터베이스에 등록
    • findUserByUsername을 사용해서 같은 username이 있으면 해당 값을 반환
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.web.study.repository.UserRepository">
    	<resultMap type="com.web.study.domain.entity.Role" id="role">
    		<result column="role_id" property="role_id"/>
    		<result column="role_name" property="role_name"/>
    	</resultMap>
    	
    	<resultMap type="com.web.study.domain.entity.Authority" id="authority">
    		<result column="authority_id" property="authority_id"/>
    		<result column="user_id" property="user_id"/>
    		<result column="role_id" property="role_id"/>
    		<collection property="role" resultMap="role"/>
    	</resultMap>
    	
    	<resultMap type="com.web.study.domain.entity.User" id="user">
    		<result column="user_id" property="user_id"/>
    		<result column="username" property="username"/>
    		<result column="password" property="password"/>
    		<result column="name" property="name"/>
    		<result column="email" property="email"/>
    		<collection property="authorities" javaType="list" resultMap="authority"></collection>
    	</resultMap>
        
    	<insert 
    	id="saveUser" 
    	parameterType="com.web.study.domain.entity.User"
    	useGeneratedKeys="true"
    	keyProperty="user_id">
    		insert into user_mst
    		values(0, #{username}, #{password}, #{name}, #{email})
    	</insert>
    	
    	<insert id="addAuthorities" parameterType="list">
    		insert into authority_mst
    		values
    		<foreach collection="list" item="authority" separator=",">
    			(0, #{authority.user_id}, #{authority.role_id})
    		</foreach>
    	</insert>
        
    	<select id="findUserByUsername" resultMap="user">
    		select 
    			um.user_id,
    			um.username,
    			um.password,
    			um.name,
    			um.email,
    			am.authority_id,
    			am.role_id,
    			rm.role_name
    		from 
    			user_mst um
    			left outer join authority_mst am on(am.user_id = um.user_id)
    			left outer join role_mst rm on(rm.role_id = am.role_id)
    		where
    			um.username = #{username}
    	</select>
    	
    </mapper>

     

    3. 로그인

    • 로그인할 정보를 입력(username, password)
    • 해당 정보로 로그인 요청
    • AuthenticationManager에게 username, password를 전달
    • AuthenticationManager가 인증을 시작
    • UserDetailsService의 loadUserByUsername(String username)이 호출
    • UserDetails를 리턴받아서 Authentication객체를 생성(username으로 DB에서 조회된 UserEntity가 없으면 예외 처리)
    • Authentication 객체가 생성되면 로그인 성공(Security에서 인증 절차)
    • 토큰을 제공하기위해 Authentication객체를 JWT로 변환하는 작업을 진행
    • 변환된 JWT를 클라이언트에게 응답
    • 클라이언트는 JWT 토큰을 로컬스토리지나 쿠키에 저장

    ■ PrincipalUserDetails

    • PrincipalUserDetails 클래스는 인증이 완료된 유저의 정보를 모아두는 객체
    • UserDetails를 구현하는 클래스
    • 사용기간 만료, 계정 잠금, 비밀번호 5회 틀림, 계정 비활성화 등을 설정 가능
    package com.web.study.security;
    
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import lombok.Builder;
    import lombok.Getter;
    
    // security 라이브러리가 구현해줌
    @Builder
    @Getter
    public class PrincipalUserDetails implements UserDetails{
    
    	private static final long serialVersionUID = -1913596722176708482L;
    	
    	private int userId;
    	private String username;
    	private String password;
    	private List<String> roles;
    
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    		roles.forEach(role -> {
    			authorities.add(new SimpleGrantedAuthority(role));
    		});
    		return authorities;
    	}
    
    	@Override
    	public String getPassword() {
    		return password;
    	}
    
    	@Override
    	public String getUsername() {
    		return username;
    	}
    
    	// 사용기간 만료
    	@Override
    	public boolean isAccountNonExpired() {
    		return true;
    	}
    
    	// 계정을 잠궈버림
    	@Override
    	public boolean isAccountNonLocked() {
    		return true;
    	}
    
    	// 비밀번호 5회 틀렸을 때
    	@Override
    	public boolean isCredentialsNonExpired() {
    		return true;
    	}
    
    	// 계정 비활성 상태(이메일 인증을 완료해야하거나, 전화번호 인증을 하지 않았을 때)
    	@Override
    	public boolean isEnabled() {
    		return true;
    	}
    
    }

    ■ PrincipalDetailsService

    • UserDetailsService를 구현하는 클래스
    • loadUserByUsername을 사용해서 로그인한 정보가 데이터베이스에 존재하는지 확인
    • 인증 성공 시 인증된 사용자를 리턴 값으로 제공
    package com.web.study.security;
    
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.stereotype.Service;
    
    import com.web.study.domain.entity.User;
    import com.web.study.exception.CustomException;
    import com.web.study.repository.UserRepository;
    
    import lombok.RequiredArgsConstructor;
    
    @Service
    @RequiredArgsConstructor
    public class PrincipalDetailsService implements UserDetailsService {
    
    	private final UserRepository userRepository;
    	
    	@Override
    	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    		User userEntity = userRepository.findUserByUsername(username);
    		
    		if(userEntity == null) {
    			throw new CustomException("사용자 정보를 다시 확인해주세요.");
    		}
    		return userEntity.toPrincipal();
    	}
    
    }

    ■ LoginReqDto

    • 로그인 시 username과 password 값을 받음
    package com.web.study.dto.auth;
    
    import lombok.Data;
    
    @Data
    public class LoginReqDto {
    	private String username;
    	private String password;
    }

    ■ AuthController

    • 로그인 요청은 /auth/login로 Post 요청으로 로그인 진행
    • 로그인은 등록하는 것이 아니지만 정보들이 url에 노출되면 안되기 때문에 Post 요청으로 진행
    package com.web.study.controller.auth;
    
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.web.study.dto.DataResponseDto;
    import com.web.study.dto.ResponseDto;
    import com.web.study.dto.auth.LoginReqDto;
    import com.web.study.dto.auth.RegisteUserReqDto;
    import com.web.study.service.AuthService;
    
    import lombok.RequiredArgsConstructor;
    
    @RestController
    @RequiredArgsConstructor
    public class AuthController {
    	private final AuthService authService;
    	
    	@PostMapping("/auth/register")
    	public ResponseEntity<? extends ResponseDto> registe(@RequestBody RegisteUserReqDto registeUserReqDto) {
    		authService.duplicatedUsername(registeUserReqDto);
    		authService.registeUser(registeUserReqDto);
    		return ResponseEntity.ok().body(ResponseDto.ofDefault());
    	}
    	
    	@PostMapping("/auth/login")
    	public ResponseEntity<? extends ResponseDto> login(@RequestBody LoginReqDto loginReqDto) {
    		return ResponseEntity.ok().body(DataResponseDto.of(authService.login(loginReqDto)));
    	}
    }

    ■ AuthService

    package com.web.study.service;
    
    import com.web.study.dto.auth.LoginReqDto;
    import com.web.study.dto.auth.RegisteUserReqDto;
    import com.web.study.dto.response.auth.JwtTokenRespDto;
    
    public interface AuthService {
    	public void registeUser(RegisteUserReqDto registeUserReqDto);
    	public void duplicatedUsername(RegisteUserReqDto registeUserReqDto);
    	
    	public JwtTokenRespDto login(LoginReqDto loginReqDto);
    }

    ■ AuthServiceImpl

    • authenticationManagerBuilder는 로그인하는 사용자 정보를 가지고 인증을 대신해주는 매니저 역할
      • 해당 매니저가 UsernamePasswordAuthenticationToken을 통해 매개변수를 자기가 알아볼 수 있도록 변환
    • authenticate 메소드를 통해 사용자를 인증
      • 매개변수로 authenticationToken 값을 넣어줌
      • 실행 시 자동으로 UserDetailsService에 loadUserByUsername이 실행
      • 여기서 리턴 받은 authentication(인증된 사용자 정보)을 통해 jwt 토큰 생성
    package com.web.study.service;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.core.Authentication;
    import org.springframework.stereotype.Service;
    
    import com.web.study.domain.entity.Authority;
    import com.web.study.domain.entity.User;
    import com.web.study.dto.auth.LoginReqDto;
    import com.web.study.dto.auth.RegisteUserReqDto;
    import com.web.study.dto.response.auth.JwtTokenRespDto;
    import com.web.study.exception.CustomException;
    import com.web.study.repository.UserRepository;
    import com.web.study.security.jwt.JwtTokenProvider;
    
    import lombok.RequiredArgsConstructor;
    
    @Service
    @RequiredArgsConstructor
    public class AuthServiceImpl implements AuthService{
    	private final UserRepository userRepository;
    	private final AuthenticationManagerBuilder authenticationManagerBuilder;
    	private final JwtTokenProvider jwtTokenProvider;
    	@Override
    	public void registeUser(RegisteUserReqDto registeUserReqDto) {
    		User userEntity = registeUserReqDto.toEntity();
    		
    		userRepository.saveUser(userEntity);
    		
    		List<Authority> authorities = new ArrayList<>();
    		authorities.add(Authority.builder().user_id(userEntity.getUser_id()).role_id(1).build());
    		
    		userRepository.addAuthorities(authorities);
    	}
    
    	@Override
    	public void duplicatedUsername(RegisteUserReqDto registeUserReqDto) {
    		User userEntity = userRepository.findUserByUsername(registeUserReqDto.getUsername());
    		
    		if(userEntity != null) {
    			Map<String, String> errorMap = new HashMap<>();
    			errorMap.put("username", "이미 사용중인 사용자이름입니다.");
    			
    			throw new CustomException("중복 검사 오류", errorMap);
    		}
    	}
    
    	@Override
    	public JwtTokenRespDto login(LoginReqDto loginReqDto) {
    		
    		// security가 알아볼수 있는 형태로 username과 password를 변환(매니저가 알아먹을수 있는 형태)
    		UsernamePasswordAuthenticationToken authenticationToken = 
    				new UsernamePasswordAuthenticationToken(loginReqDto.getUsername(), loginReqDto.getPassword());
    		
    		// 인증이 완료가 되면 authentication 객체를 생성
    		// 실제 로그인 정보를 authentication안에서 관리
    		// UserDetailsService의 loadUserByUsername()이 호출이 된다!!!
    		// authentication에 Principal로 업 캐스팅 되어 적용
    		Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
    		return jwtTokenProvider.createToken(authentication);
    	}
    }

    ■ JwtTokenProvider

    • 생성자에 @Value 어노테이션을 사용해서 yml에 작성한 secretKey값을 매개변수로 받아와 암호화하여 key 멤버변수에 대입
    • createToken 메소드를 사용하여 토큰 생성
      • 매개변수로 받는 authentication은 인증된 사용자의 정보를 의미
      • authentication에서 받은 권한들을 ,를 사용해서 하나의 문자열로 만들어줌
      • tokenExpiresDate변수로 토큰 만료시간 생성
      • authentication에서 getPrincipal을 사용하여 userDetails 변수에 대입
    • Jwts.builder()를 통해 토큰 생성
      • setSubject에는 토큰의 이름을 지정하며 해당 코드에서는 username으로 지정
      • claim에는 userId와 auth에 해당하는 값을 해당 유저의 userId와 권한들로 지정
      • setExpiration에는 위에서 생성한 토큰 만료시간을 지정
      • signWith에서는 key값을 HS256으로 암호화 진행
      • compact()를 통해 토큰 생성
    • 리턴 값에는 생성한 토큰의 앞에 Bearer를 앞에 붙여서 리턴
    package com.web.study.security.jwt;
    
    import java.security.Key;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import com.web.study.dto.response.auth.JwtTokenRespDto;
    import com.web.study.exception.CustomException;
    import com.web.study.security.PrincipalUserDetails;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.MalformedJwtException;
    import io.jsonwebtoken.SignatureAlgorithm;
    import io.jsonwebtoken.UnsupportedJwtException;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import io.jsonwebtoken.security.SecurityException;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    @Component
    public class JwtTokenProvider {
    
    	private final Key key;
    	
    	// component일 때 객체가 생성될 때 application.yml에서 값을 가져올 수 있음
    	public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey) {
    		byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    		this.key = Keys.hmacShaKeyFor(keyBytes);
    	}
    	
    	public JwtTokenRespDto createToken(Authentication authentication) {
    		
    		StringBuilder authoritiesBuilder = new StringBuilder();
    		
    		authentication.getAuthorities().forEach(grantedAuthority -> {
    			authoritiesBuilder.append(grantedAuthority.getAuthority());
    			authoritiesBuilder.append(",");
    		});
    		
    		authoritiesBuilder.delete(authoritiesBuilder.length() - 1 , authoritiesBuilder.length());
    		
    		String authorities = authoritiesBuilder.toString();
    		
    		long now = (new Date()).getTime();
    		// 1000 == 1초
    		Date tokenExpiresDate = new Date(now + (1000 * 60 * 30)); // 토큰 만료 시간
    		
    		PrincipalUserDetails userDetails = (PrincipalUserDetails) authentication.getPrincipal();
    		
    		String accessToken = Jwts.builder()
    				.setSubject(authentication.getName())
    				.claim("userId", userDetails.getUserId())
    				.claim("auth", authorities)
    				.setExpiration(tokenExpiresDate)
    				.signWith(key, SignatureAlgorithm.HS256)
    				.compact();
    		
    		return JwtTokenRespDto.builder()
    				.grantType("Bearer")
    				.accessToken(accessToken)
    				.build();
    	}
    	
    	public boolean validateToken(String token) {
    		try {
    			// 토큰이 어떤 토큰인지 알려주는 라이브러리
    			Jwts.parserBuilder()
    			.setSigningKey(key)
    			.build()
    			.parseClaimsJws(token);
    			
    			return true;
    		} catch (SecurityException | MalformedJwtException e) {
    			// Security 라이브러리에 오류가 있거나, JOSN의 포맷이 잘못된 형식의 JWT가 들어왔을 때 예외
    			// SignatureExceptio9n이 포함되어 있음
    			log.info("Invalid JWT Token", e);
    		} catch (ExpiredJwtException e) {
    			// 토큰의 유효기간이 만료된 경우 예외
    			log.info("Expired JWT Token", e);
    		} catch (UnsupportedJwtException e) {
    			// jwt의 형식을 지키지 않은 경우 (Header.Payload.Signature)
    			log.info("Unsupported JWT Token", e);
    		} catch (IllegalArgumentException e) {
    			// JWT 토큰이 없을 때
    			log.info("IllegalArgument JWT Token", e);
    		} catch (Exception e) {
    			log.info("JWT Token Error", e);
    		}
    		return false;
    	}
    	
    	public Authentication getAuthentication(String accessToken) {
    		Claims claims = parseClaims(accessToken);
    		Object roles = claims.get("auth");
    		
    		if(roles == null) {
    			throw new CustomException("권한 정보가 없는 토큰입니다.");
    		}
    		
    		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    		String[] rolesArray = roles.toString().split(",");
    		Arrays.asList(rolesArray).forEach(role -> {
    			authorities.add(new SimpleGrantedAuthority(role));
    		});
    		
    		UserDetails userDetails = new User(claims.getSubject(),"",authorities); // username, password, authorities
    		
    		return new UsernamePasswordAuthenticationToken(userDetails,"",authorities);
    	}
    	
    	private Claims parseClaims(String accessToken) {
    		try {
    			return Jwts.parserBuilder()
    					.setSigningKey(key)
    					.build()
    					.parseClaimsJws(accessToken)
    					.getBody();
    			
    		} catch (ExpiredJwtException e) {
    			return e.getClaims();
    		}
    	}
    }

    ■ JwtTokenRespDto

    package com.web.study.dto.response.auth;
    
    import lombok.Builder;
    import lombok.Data;
    
    @Builder
    @Data
    public class JwtTokenRespDto {
    	private String grantType;
    	private String accessToken;
    }

     

    4. 요청 토큰 인증(JWT 토큰)

    • 요청 Header에 Bearer 방식으로 JWT 토큰을 전달
    • Security에서 인증이 필요한 요청들은 JwtAuthenticationFilter를 통해 인증절차를 진행
    • 인증의 최종목표는 SecurityContextHolder 객체의 Context에 Authentication 객체 등록(인증 성공)
    • JwtAuthenticationFilter에서 요청 Header에 들어있는 Authorization의 JWT토큰을 추출
    • JWT 토큰을 검증(검증 실패시 Exception이 생성되며 AuthenticationEntryPoint가 실행되면서 401응답)
    • JWT 토큰 검증이 완료되면 JWT 토큰에서 Claims를 추출
    • Claims에서 username과 Authorities를 추출하여 Authentication 객체를 생성
    • 생성된 Authentication객체를 SecurityContext에 등록
    • 등록이 완료되면 다음 doChain이 호출 

    ■ JwtTokenProvider

    • validateToken 메소드를 통해 요청 받은 토큰 값이 유효한지 테스트
    • Jwts.parserBuilder를 사용하여 해당 토큰이 어떤 토큰인지 확인
    • 토큰이 유효하면 true를 반환하고 예외 발생 시 아래 코드의 catch를 통해 예외 처리
    • getAuthentication 메소드를 사용하여 검증된 토큰을 Authentication으로 변환
      • accessToken을 매개변수로 받음
    • parseClaims 메소드를 생성하여 토큰을 Claims 형태로 변환
      • Claims에서 권한만 콤마를 기준으로 배열화한 뒤 리스트로 변환
      • 권한이 없으면 권한 정보가 없는 토큰이라고 예외 처리
    • User에 username, password, authorities를 넣어 생성 후 UserDetails에 대입(해당 코드는 password는 빈값)
    • UsernamePasswordAuthenticationToken에 userDetails 객체, 인증 관련 값, 권한을 넣어서 생성 후 리턴
      • 임시의 Authentication을 생성하여 리턴하고 필터에서 Authentication으로 업캐스팅 됨
    package com.web.study.security.jwt;
    
    import java.security.Key;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Date;
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.stereotype.Component;
    
    import com.web.study.dto.response.auth.JwtTokenRespDto;
    import com.web.study.exception.CustomException;
    import com.web.study.security.PrincipalUserDetails;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.ExpiredJwtException;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.MalformedJwtException;
    import io.jsonwebtoken.SignatureAlgorithm;
    import io.jsonwebtoken.UnsupportedJwtException;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import io.jsonwebtoken.security.SecurityException;
    import lombok.extern.slf4j.Slf4j;
    
    @Slf4j
    @Component
    public class JwtTokenProvider {
    
    	private final Key key;
    	
    	// component일 때 객체가 생성될 때 application.yml에서 값을 가져올 수 있음
    	public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey) {
    		byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    		this.key = Keys.hmacShaKeyFor(keyBytes);
    	}
    	
    	public JwtTokenRespDto createToken(Authentication authentication) {
    		
    		StringBuilder authoritiesBuilder = new StringBuilder();
    		
    		authentication.getAuthorities().forEach(grantedAuthority -> {
    			authoritiesBuilder.append(grantedAuthority.getAuthority());
    			authoritiesBuilder.append(",");
    		});
    		
    		authoritiesBuilder.delete(authoritiesBuilder.length() - 1 , authoritiesBuilder.length());
    		
    		String authorities = authoritiesBuilder.toString();
    		
    		long now = (new Date()).getTime();
    		// 1000 == 1초
    		Date tokenExpiresDate = new Date(now + (1000 * 60 * 30)); // 토큰 만료 시간
    		
    		PrincipalUserDetails userDetails = (PrincipalUserDetails) authentication.getPrincipal();
    		
    		String accessToken = Jwts.builder()
    				.setSubject(authentication.getName())
    				.claim("userId", userDetails.getUserId())
    				.claim("auth", authorities)
    				.setExpiration(tokenExpiresDate)
    				.signWith(key, SignatureAlgorithm.HS256)
    				.compact();
    		
    		return JwtTokenRespDto.builder()
    				.grantType("Bearer")
    				.accessToken(accessToken)
    				.build();
    	}
    	
    	public boolean validateToken(String token) {
    		try {
    			// 토큰이 어떤 토큰인지 알려주는 라이브러리
    			Jwts.parserBuilder()
    			.setSigningKey(key)
    			.build()
    			.parseClaimsJws(token);
    			
    			return true;
    		} catch (SecurityException | MalformedJwtException e) {
    			// Security 라이브러리에 오류가 있거나, JOSN의 포맷이 잘못된 형식의 JWT가 들어왔을 때 예외
    			// SignatureExceptio9n이 포함되어 있음
    			log.info("Invalid JWT Token", e);
    		} catch (ExpiredJwtException e) {
    			// 토큰의 유효기간이 만료된 경우 예외
    			log.info("Expired JWT Token", e);
    		} catch (UnsupportedJwtException e) {
    			// jwt의 형식을 지키지 않은 경우 (Header.Payload.Signature)
    			log.info("Unsupported JWT Token", e);
    		} catch (IllegalArgumentException e) {
    			// JWT 토큰이 없을 때
    			log.info("IllegalArgument JWT Token", e);
    		} catch (Exception e) {
    			log.info("JWT Token Error", e);
    		}
    		return false;
    	}
    	
    	public Authentication getAuthentication(String accessToken) {
    		Claims claims = parseClaims(accessToken);
    		Object roles = claims.get("auth");
    		
    		if(roles == null) {
    			throw new CustomException("권한 정보가 없는 토큰입니다.");
    		}
    		
    		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
    		String[] rolesArray = roles.toString().split(",");
    		Arrays.asList(rolesArray).forEach(role -> {
    			authorities.add(new SimpleGrantedAuthority(role));
    		});
    		
    		UserDetails userDetails = new User(claims.getSubject(),"",authorities); // username, password, authorities
    		
    		return new UsernamePasswordAuthenticationToken(userDetails,"",authorities);
    	}
    	
    	private Claims parseClaims(String accessToken) {
    		try {
    			return Jwts.parserBuilder()
    					.setSigningKey(key)
    					.build()
    					.parseClaimsJws(accessToken)
    					.getBody();
    			
    		} catch (ExpiredJwtException e) {
    			return e.getClaims();
    		}
    	}
    }

    ■ JwtAuthenticationFilter

    • GenericFilterBean클래스를 상속
    • JWT 토큰이 유효한지 체크하기 위한 필터
    • doFilter 메소드에 전처리만 적용
    • 요청 시 받은 토큰을 token변수를 생성하여 대입
      • getToken을 통하여 Bearer를 제거한 토큰 값을 가져옴
    • 해당 토큰이 알맞는지 확인하기 위해 validateToken메소드에 해당 토큰을 매개변수로 적용
    • 유효할 경우 getAuthentication 메소드를 통해 토큰으로 Authentication 객체 생성
    • SecurityContextHolder에 Context 내에 Authentication 값이 들어가게 되면 인증 완료
    package com.web.study.security.jwt;
    
    import java.io.IOException;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.GenericFilterBean;
    
    import lombok.RequiredArgsConstructor;
    
    @RequiredArgsConstructor
    public class JwtAuthenticationFilter extends GenericFilterBean{
    
    	private final JwtTokenProvider jwtTokenProvider;
    	
    	@Override
    	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    			throws IOException, ServletException {
    		
    		String token = getToken(request);
    		
    		System.out.println("token: " + token);
    		
    		boolean validationFlag = jwtTokenProvider.validateToken(token);
    		System.out.println("flag: " + validationFlag);;
    		
    		if(validationFlag) {
    			Authentication authentication = jwtTokenProvider.getAuthentication(token);
    			// SecurityContext에 Authentication객체가 들어있으면 로그인 인증된것
    			SecurityContextHolder.getContext().setAuthentication(authentication);
    		}
    		
    		chain.doFilter(request, response);
    	}
    
    	private String getToken(ServletRequest request) {
    		HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    		String type = "Bearer";
    		String token = httpServletRequest.getHeader("Authorization");
    		
    		// hasText 문자열이 Null, 공백이 아닌지 확인
    		if(StringUtils.hasText(token) && token.startsWith(type)) {
    			return token.substring(type.length() +1);
    		}
    		return null;
    	}
    }

    ■ JwtAuthenticationEntryPoint

    • AuthenticationEntryPoint를 구현하는 클래스
    • 자원에 대한 요청이 인증되지 않을 경우 commence() 메소드가 호출되어 인증 실패에 대한 에러 처리를 함
    • response에서 setContentType을 JSON형태로 지정 후 상태를 401로 설정 후 PrintWriter에 대입
    • ErrorResponseDto를 통해 해당 오류 정보를 출력
    package com.web.study.security.jwt;
    
    import java.io.IOException;
    import java.io.PrintWriter;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.web.study.dto.ErrorResponseDto;
    
    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint{
    
    	@Override
    	public void commence(HttpServletRequest request, HttpServletResponse response,
    			AuthenticationException authException) throws IOException, ServletException {
    
    		response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    		response.setStatus(HttpStatus.UNAUTHORIZED.value());
    		PrintWriter out = response.getWriter();
    		ObjectMapper responseJson = new ObjectMapper();
    		out.println(responseJson.writeValueAsString(ErrorResponseDto.of(HttpStatus.UNAUTHORIZED, authException)));
    	}
    
    }