UserDetails
를 구현한 클래스와 JpaRepository
를 상속하고 findByUsername(String member)
를 정의한 레포지토리, UserDetailsService
를 구현한 서비스가 필요합니다. 해당 클래스는 이미 수차례 정의한 적이 있습니다.
JWT 로그인을 처리하기 위해서는 UsernamePasswordAuthenticationFilter
를 상속하여 attemptAuthentication()
successfulAuthentication()
메서드를 구현해야 합니다.
@RequiredArgsConstructor
public class 토큰생성필터 extends UsernamePasswordAuthenticationFilter{
private final AuthenticationManager authenticationManager;
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{
try{
ObjectMapper 매퍼 = new ObjectMapper();
엔티티 엔티티 = 매퍼.readValue(request.getInputStream(),엔티티.class);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(엔티티.getUsername(), 엔티티.getPassword());
Authentication authentication = authenticationManager.authenticate(authenticationToken);
return authentication;
}catch(IOException e){
e.printStackTrace();
}
return null;
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserDetails클래스 userDetals = (UserDetails클래스) authResult.getUserDetails();
String 토큰 = JWT.create()
.withSubject("cos jwt token")
.withExpiresAt(new Date(System.currentTimeMillis() + (60 * 1000 * 10)))
.withClaim("id", userDetals.getMember().getId())
.withClaim("username", userDetals.getMember().getUsername())
.sign(Algorithm.HMAC512("cos_jwt_token"));
response.addHeader("Authorization","Bearer " + 토큰);
}
}
attemptAuthentication(HttpServletRequest, HttpServletResponse)
: 사용자의 로그인 입력 정보를 받아들여 AuthenticationManager에게 전달하고 Authentication을 반환합니다.
ObjectMapper
: 사용자로 부터의 JSON입력 정보를 객체로 매핑하기 위해 사용합니다.
UsernamePasswordAuthenticationToken
: 사용자로부터 입력받은 ID와 비밀번호를 저장하는 인증 객체입니다.
successfulAuthentication()
: 인증 성공시에 추가적인 처리를 지정할 수 있습니다. 위의 메서드는 토큰을 생성하여 응답에 추가하였습니다.
Authentication.getUserDetails()
: UserDetails를 구현한 클래스의 객체를 반환합니다.
JWT.create()
: JWT 토큰을 생성합니다.
JWT.withSubject()
: JWT 토큰의 주제를 설정합니다.
JWT.withExpiresAt(new Date(System.currentTimeMillis() + (60 * 1000 * 10)))
: 현재 시간으로 부터 10분(1000ms 60초 10분)후에 토큰이 만료됩니다.
JWT.withClaim("키","깂")
: 키와 값 쌍을 토큰에 추가합니다.
JWT.sign(Algorithm.HMAC512("비밀키"))
: 비밀키
가지고 HMAC512 알고리즘으로 토큰의 Signature 부분을 암호화 합니다.(확인용)
response.addHeader("Authorization", "접두어 " + 토큰);
: 응답 헤더의 Authorization에 접두어를 추가한 토큰을 추가합니다.
public class 토큰이용필터 extends BasicAuthenticationFilter{
private 레포지토리 레포지토리;
public 토큰이용필터(AuthenticaitonManager authenticationManager, 레포지토리 레포지토리){
super(authenticationManager);
this.레포지토리 = 레포지토리;
}
String 헤더 = request.getHeader("Authorization");
if(헤더 == null || !해더.startsWith("접두어")){
chain.doFilter(request,response);
return;
}
String 토큰 = 헤더.replace("접두어","");
String username = JWT.require(Algorithm"HMAC512("비밀키")).build.verify(토큰).getClaim("username").asString();
if(username != null){
엔티티 엔티티 = 레포지토리.findByUsername(username);
UserDetails클래스 userDetails = new UserDetails클래스(엔티티);
Authentication authenticaiton = new UsernamePasswordAuthenticationToken(principalDetails,null,principalDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request,response);
}
super.doFilterInternal(request,response,chain);
}
doFilterInternal()
: BasicAuthenticationFilter
는 토큰 인증시 사용되는 필터 이며 메서드는 토큰인증을 위해 오버라이딩합니다.HttpServletRequest.getHeader("Authroization")
: 헤더의 Authoriaiotn 파라미터의 값을 읽어들입니다. 이후 "접두어"
가 필터가 찾는 것이 맞는지 확인하고 제거합니다. JWT.require(Alogrithm.HMAC512("비밀키")).build().verify(토큰).getClaim("username"),asString()
: 토큰을 비밀키로 디코딩 하여 username의 값을 문자열로 불러옵니다.만약 username
이 존재한다면 엔티티
를 찾아 UserDetails
객체로 만들어 Authentication
을 생성하여 SecurityContextHolder
에 저장하고 다음 필터를 실행합니다.
필터를 만들기만 한다고 원하는 때에 실행되지는 않습니다. 필터를 필터 체인에 추가하여야만 원하는 때에 필터가 작동합니다.
필터는 설정클래스의 SecurityFilterChain
(스프링 필터체인)빈 객체에 추가합니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class 설정클래스{
private final CorsFilter corsFilter;
private final 레포지토리 레포지토리;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.csrf().disable()
...
.aply(new CustomDsl())
.and()
...
}
public class CustomDsl extends AbstractHttpConfigurer<CustomDsl,HttpSecurity>{
public void configure(HttpSecurity builder) throws Exception{
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
builder
.addFilter(corsFilter)
.addFilter(new 토큰생성필터(authenticationManager));
.addFilter(new 토큰이용필터(authenticationManager, 레포지토리));
}
}
}
class customDsl extends AbstarctHttpConfigurer<CustomDsl, HttpSecurity>
: SpringSecurity가 버전업을 하면서 더이상 직접적으로 addFilter()
메서드로 추가하는 것이 deprecated 되었습니다. 따라서 AbstractHttpConfigurer
클래스를 상속하는 클래스를 만들어 apply()
합니다.configure(HttpSecurity)
: 필터에 추가할 내용을 정의합니다.HttpSecurity.getSharedObject(AuthenticationManager.class)
: AuthenticationManager을 반환 받습니다.HttpSecurity.addFilter(new 필터)
: 필터를 필터체인에 추가합니다.클라이언트로 부터 JSON정보를 받아와 토큰으로 만들고 만든 토큰을 이용하여 인증도 해보았습니다. 세션과 함께 토큰도 많이 사용되므로 알아두는것이 좋습니다.
https://github.com/ds02168/CodeStates_Spring/tree/main/section4-week1-THR