
이번편은 스프링 시큐리티 동작 방식을 이해하고 실제 어떤식으로 구현해야 할지 알아본다.
1. 스프링 시큐리티 동작방식
앞서 그 전 포스터에서도 게시했지만 스프링 시큐리티는 로그인 방식은 간략하게 이렇게 이루어진다.

1.흐름 방식
사용자가 username과 password를 준다.
● DelegatingFilterProxy
● 서블릿 필터체인에 들어오는 HTTP 요청 ( 유저 정보 , url 경로)등을 FilterChainProxy에 위임한다
● 이는 스프링 빈으로 등록된 필터등을 서블릿 환경에서 사용할 수 있게 해준다.
● FilterChainProxy
●SecurityFilterChain을 관리한다. 이는 여러 보안 필터를 순서대로 적용하는 역활을 함.

● AuthenticationFilter
●이 필터에 들어오면 UsernamePasswordAuthenticationFilter가 실행됨.
●이 필터는 기본적으로 로그인 환경에서만 실행된다.
● jwt만들기 위해서 이 유저에 정보들을 이 필터에서 받아
UsernamePasswordAuthenticationToken을 만들어 줘야 한다.

● 토큰은 2개의 생성자가 있는데 setAuthenticated(false)는 인증이 되지 않았음이고
true일 경우는 인증됨인데
위에 동작 방식처럼 필터를 한바퀴 돌아서 인증을 하기 때문에
처음에는 false -> true로 바뀌는 과정이다.
● 이 토큰을 false인 상태에서 유저 아이디와 비밀번호를 받고 Manager에 넘겨줌
●AuthenticationManager
●이 클래스는 해당 방식이 Oauth2 인지 , Form-data 방식인지 등을 결정해서
이에 적절한 방식을 내려 AuthenticationProvider에게 전달한다.

●AuthenticationProvider
●이 클래스로 넘어오게 되면 UserDetailsSerivce를 호출한다.
●이 때 UserDetailsService에는 username만 인자로 넘겨주게 되는데
이는 이 클래스에서는 유저 아이디만을 검증하기 때문이다.
(이유는 Bcry 등이나 Base64로 암호화가 되어있으면 이를 풀 방법이 없기 때문)
●해당클래스에서 개발자는 오버라이딩해서 DB에서 유저의 정보들을 찾아서
UserDetails 타입으로 찾은 유저의 객체 정보를 넘겨줘야 한다.
●UserDetails
이 객체에서는 UserDetailsService에서 받은 객체 정보를 저장한다.
이때의 객체 정보들은 다시 위에서부터 밑에서 인증이 진행될때 참조되는 객체이다.

UserDetails까지 끝났다면 이제부터는 밑에서 위로 인증방식이 진행된다.
●AuthenticationProvider
●UserDetails에 저장되어있는 인증을 값들을 기반으로 인증을 확인한다.
●이 클래스는 1. 아이디와 비밀번호를 비교하는데
1. UserDetails에 있는 아이디와 비밀번호를 꺼냄
2.유저가 입력한 정보를 가지고 옴 ( 아이디 비밀번호)
● 1번과 2번을 통하여 해당 과정을 비교하는데 일치한다면
Authentication객체를 생성한다.

이 부분에 해당된다.
●AuthenticationFilter
● UsernamePasswordFilter가 호출되고 위에서 말한 false에서 true로 바뀌는 과정이된다.

setAuthenticated(true)로 반환되며 Authentication해당 객체는 인증 됨으로 바뀐다.

●SecurityContext
● Authentication이 인증됨으로 바뀌면 SecurityContext 이 객체의
우리가 담은 UserDetails의 정보를 담는다.
● 위에 과정이 끝낫다면 SecurityContext에는 Authentication 객체가 담기고
이를 감싸는 SecurityContextHolder가 생성되며 이로써 인증은 끝나게 됨.
[Jwt 토큰 만들기]
이제부터 본격적으로 jwt 토큰을 만들어보자.
(만들기전 jpa테이블과 Repository는 스스로 작성할 것)
하나하나 차근차근 밟아보도록한다.
package com.example.springsecurity04.Security;
import com.example.springsecurity04.Filter.JwtCheckFilter;
import com.example.springsecurity04.Filter.UserCheckFilter;
import com.example.springsecurity04.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtConfig {
private final UserRepository repository;
@Bean
SecurityFilterChain filterChain(HttpSecurity security,AuthenticationConfiguration configuration) throws Exception {
security.httpBasic(AbstractHttpConfigurer :: disable);
security.formLogin(AbstractHttpConfigurer :: disable);
security.csrf(AbstractHttpConfigurer ::disable);
security.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
security.addFilterBefore(new UserCheckFilter(configuration.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class);
security.addFilterBefore(new JwtCheckFilter(repository), UsernamePasswordAuthenticationFilter.class);
security.authorizeHttpRequests(request -> request.antMatchers("/join","/managerjoin").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/manager/**").hasAnyRole("ADMIN","MANAGER")
.antMatchers("/user/**").hasAnyRole("ADMIN","MANAGER","USER")
.anyRequest().authenticated());
return security.build();
}
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
일단 시작하기에 앞서 Config부분을 이러한 형태로 만들어놓자.
security.httpBasic(AbstractHttpConfigurer :: disable);
security.formLogin(AbstractHttpConfigurer :: disable);
security.csrf(AbstractHttpConfigurer ::disable);
security.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
1.CSRF 기능을 끄고
2.FormLogin 방식은 검증하지 않게 한다.
3. JWT토큰을 사용할것이기 떄문에 세션을 끈다.
security.addFilterBefore(new UserCheckFilter(configuration.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class);
security.addFilterBefore(new JwtCheckFilter(repository), UsernamePasswordAuthenticationFilter.class);
jwt 토큰을 체크하기위해 Filter부분을 만든다.
추가 설명은 밑에서 하기에 UserCheckFilter부분에 생성자로
AuthenticationManager과 DB 의존성을 받기 위해 생성자에 만들어 놓는다.
security.authorizeHttpRequests(request -> request.antMatchers("/join","/managerjoin").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/manager/**").hasAnyRole("ADMIN","MANAGER")
.antMatchers("/user/**").hasAnyRole("ADMIN","MANAGER","USER")
.anyRequest().authenticated());
해당 url 경로에 인증과 인가 방식을 놓는다.
1.사용자가 아이디와 비밀번호를 제공하였다.
사용자와비밀번호가 들어오게 되면 usernamepasswordFilter에서 받게된다.
그러므로 즉 이 클래스를 구현해야 한다.
package com.example.springsecurity04.Filter;
import com.example.springsecurity04.Dto.UserDto;
import com.example.springsecurity04.JwtUtil.JsonWebToken;
import com.example.springsecurity04.Repository.UserRepository;
import com.example.springsecurity04.Service.UserDetailsInformation;
import com.example.springsecurity04.Table.UserEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
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.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@Component
public class UserCheckFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager manager;
public UserCheckFilter(AuthenticationManager manager) {
super(manager);
this.manager = manager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
ObjectMapper mapper = new ObjectMapper();
try {
UserEntity userEntity = mapper.readValue(request.getInputStream(), UserEntity.class);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userEntity.getUsername(),userEntity.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")));
return manager.authenticate(token);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserDetailsInformation information = (UserDetailsInformation) authResult.getPrincipal();
String jwtToken = JsonWebToken.createJwtToken(information.getUsername(),information.getDto().getRole());
if(information != null) {
response.addHeader("Authorization","Bearer "+jwtToken);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
}
}
}
즉 이러한 클래스가 완성되어야 하는데 차근차근 하나씩 보도록 하자.
ObjectMapper mapper = new ObjectMapper();
JSON타입으로 들어왔을 때 JSON타입을 받아서 풀어주는 역활을 하는 클래스다.
UserEntity userEntity = mapper.readValue(request.getInputStream(), UserEntity.class);
request(유저의 정보)들이 들어온 값을 getInputStream()으로 찾고 , 변환 할 객체 명(필자의 경우 AccountDto)
클래스를 넣고 json 형식으로 들어온 객체로 변환시킨다.
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userEntity.getUsername(),userEntity.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")));
usernamePasswordAuthentcation 토큰에 위에 사항과 같이
(유저 아이디 , 비밀번호 , 권한정보) 등을 넣어준다.
매우 중요(권한 정보는 이 때에는 권한이 없는 유저 이기 때문에 ROLE_ANONYMOUS로 설정해줘야 한다)
참고로 이 필터는 로그인 외에는 동작하지 않으며
개발자가 기본 로그인 경로를 /login이라고 설정했다면 이 경로외에는 이 필터는 동작하지 않는다.
return manager.authenticate(token);
넣고 토큰을 만들었으면 다음 호출 클래스인 manager 클래스로 넘기기 위한 준비를한다.
2. Manager에서 Authentcation 토큰을 받고 Manager 에서 Provider 객체를 호출한다.
이 Provider객체가 호출 될 때 -> UserDetailsService를 호출하게 되는데
우리는 이 클래스를 구현하여 유저의 세부정보를 찾아야 한다.
package com.example.springsecurity04.Service;
import com.example.springsecurity04.Dto.UserDto;
import com.example.springsecurity04.Repository.UserRepository;
import com.example.springsecurity04.Table.UserEntity;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
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.Component;
import org.springframework.stereotype.Service;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class UserDetailsServiceCustom implements UserDetailsService {
private final UserRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDto dto;
Optional<UserEntity> userEntity = repository.findUserEntityByUsername(username);
if(userEntity.isPresent()) {
ModelMapper mapper = new ModelMapper();
dto = mapper.map(userEntity.get(), UserDto.class);
UserDetailsInformation information = new UserDetailsInformation(dto);
return information;
}else {
throw new UsernameNotFoundException("인증 처리 불가");
}
}
}
여기서부터 이제 db의 정보를 가져와서 유저의 세부정보들을 체크해야한다.
Optional<UserEntity> userEntity = repository.findUserEntityByUsername(username);
DB에 있는 값을 꺼내 유저 정보를 체크한다.
if(userEntity.isPresent()) {
ModelMapper mapper = new ModelMapper();
dto = mapper.map(userEntity.get(), UserDto.class);
값이 있다면 mapper를 통해 변환 후 UserDetailsInformation의 넣어준다(밑에 나옴)
UserDetailsInformation information = new UserDetailsInformation(dto);
그 후 변환된 객체를 UserDetailsInformation 생성자를 통해 필드 변수에 객체를 주입 한 후
return information;
리턴을 한다.
package com.example.springsecurity04.Service;
import com.example.springsecurity04.Dto.UserDto;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
@Data
public class UserDetailsInformation implements UserDetails {
private final UserDto dto;
public UserDetailsInformation(UserDto dto) {
this.dto = dto;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> {
return dto.getRole();
});
return authorities;
}
@Override
public String getPassword() {
return dto.getPassword();
}
@Override
public String getUsername() {
return dto.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;
}
}
이 클래스를 구현받아야하는데 UserDetail클래스이다.
이 클래스는 우리가 UserDetailsInformation에서 생성자에 필드 변수에 객체를 넣은
클래스인데 이 클래스를 토대로 스프링 시큐리티는 참조하게 된다. (대부분의 인증권한)
이 세부정보에 저장한 값을 토대로 Provider에서 대조를 하게된다.
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> {
return dto.getRole();
});
return authorities;
}
유저의 인가 정보를 저장 (권한을 저장한다)
@Override
public String getPassword() {
return dto.getPassword();
}
유저의 패스워드를 저장한다 (이 값이 null로 들어가게되면 인증의 실패한다.)
@Override
public String getUsername() {
return dto.getUsername();
}
유저의 아이디를 저장한다. 위와 동일함.
package com.example.springsecurity04.Security;
import com.example.springsecurity04.Filter.JwtCheckFilter;
import com.example.springsecurity04.Filter.UserCheckFilter;
import com.example.springsecurity04.Repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtConfig {
private final UserRepository repository;
@Bean
SecurityFilterChain filterChain(HttpSecurity security,AuthenticationConfiguration configuration) throws Exception {
security.httpBasic(AbstractHttpConfigurer :: disable);
security.formLogin(AbstractHttpConfigurer :: disable);
security.csrf(AbstractHttpConfigurer ::disable);
security.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
security.addFilterBefore(new UserCheckFilter(configuration.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class);
security.addFilterBefore(new JwtCheckFilter(repository), UsernamePasswordAuthenticationFilter.class);
security.authorizeHttpRequests(request -> request.antMatchers("/join","/managerjoin").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/manager/**").hasAnyRole("ADMIN","MANAGER")
.antMatchers("/user/**").hasAnyRole("ADMIN","MANAGER","USER")
.anyRequest().authenticated());
return security.build();
}
@Bean
public PasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
}
그 다음은 시큐리티 Config부분을 설정해줘야 하는데
여기서 잘봐야한다.
@Bean
AuthenticationManager manager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
우리가 클래스를 재정의한 usernamePasswordFilter부분은
재정의 할 때 Manager 의존성이 추가되지 않는다.
그렇기 때문에 우리가 직접 빈으로 등록해줘야한다.
성공 시 디비에 있는 값도 찾아야 함으로 디비도 추가해주자.
security.sessionManagement(session-> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
이 부분은 스프링 시큐리티에서 세션을 사용하지 않겟다라는 로직이다.
jwt사용한다는 것은 결국 세션 방식을 사용하지 않겠다 라는 의미이다.
security.addFilterBefore(new UserCheckFilter(configuration.getAuthenticationManager()), UsernamePasswordAuthenticationFilter.class);
1.addFilterBefore은 첫번째 인자에 내가 만든 클래스를 넣고 , 두번째 인자에는 스프링 시큐리티에서
동작하는 필터중 하나를 넣게 되면
해당 필터가 시작되기전에 내 필터가 먼저 시작되고 , 두번째 인자로 넣은 클래스는 실행되지 않는다.
이렇게 세부정보 까지 넣는다면 스프링 시큐리티는 Provider로 가서 세부정보와 토큰의 값을 비교 후
둘 다 맞다면 Authentication 객체를 만들고 이 객체에 false(인증되지 않음) 에서
true로 바뀌게 된다.
그 후 우리는 인증이 다된 경우 jwt토큰을 만들어줘야 하기에
package com.example.springsecurity04.Filter;
import com.example.springsecurity04.Dto.UserDto;
import com.example.springsecurity04.JwtUtil.JsonWebToken;
import com.example.springsecurity04.Repository.UserRepository;
import com.example.springsecurity04.Service.UserDetailsInformation;
import com.example.springsecurity04.Table.UserEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
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.authority.SimpleGrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@Component
public class UserCheckFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager manager;
public UserCheckFilter(AuthenticationManager manager) {
super(manager);
this.manager = manager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
ObjectMapper mapper = new ObjectMapper();
try {
UserEntity userEntity = mapper.readValue(request.getInputStream(), UserEntity.class);
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userEntity.getUsername(),userEntity.getPassword(),
List.of(new SimpleGrantedAuthority("ROLE_ANONYMOUS")));
return manager.authenticate(token);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserDetailsInformation information = (UserDetailsInformation) authResult.getPrincipal();
String jwtToken = JsonWebToken.createJwtToken(information.getUsername(),information.getDto().getRole());
if(information != null) {
response.addHeader("Authorization","Bearer "+jwtToken);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
}
}
}
다시 usernamepasswordFilter로 들어가
인증에 성공되었으면 호출하는 로직 sucessfulAuthentication 메서드를 오버라이딩한다.
하나하나 클래스를 살펴보자
UserDetailsInformation information = (UserDetailsInformation) authResult.getPrincipal();
인증에 성공한 authentication 객체 (성공이 되었다면 이 객체가 존재한다)
이 Authentication 객체는 Object타입이기 때문에 다운캐스팅을 해줘야하는데
이 때 다운캐스팅할 타겟은 UserDetailsInformation이다.
String jwtToken = JsonWebToken.createJwtToken(information.getUsername(),information.getDto().getRole());
그 후 성공을 했을 때 jwt토큰을 생성해줘야하는데..
나는 jwtCreate라는 클래스를 만들어 static으로 불러왔다.
package com.example.springsecurity04.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Date;
public class JsonWebToken {
private final static SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public static String createJwtToken(String username,String role){
Claims claims = Jwts.claims();
claims.put("username",username);
claims.put("role",role);
return Jwts.builder().setClaims(claims).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + (60000 * 30 * 24)))
.signWith(key).compact();
}
public static Claims getUsername(String token){
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
클래스 내부는 이렇게 되어있고
Claims claims = Jwts.claims();
Claims라는 jwt라는 클래스를 위한 클래스로써 , claims()는 비어있는 인스턴스를 생성하는데 ,
Clamis는 토큰 내부에 저장되어있는 다양한 정보등을 저장한다.(사용자 권한 , 유효기간 등)
즉 간단히 말해 , 이 코드는 JWT 내부 정보를 저장할 수 있는 빈 Camis 객체를 생성하고
사용자의 아이디나 역할 같은 정보를 JWT 토큰에 담아 인증이나 권한 부여에 사용될 수 있다.
claims.put("username",username);
claims.put("role",role);
여기에 유저의 세부정보
(jwt토큰으로 암호화할 정보들을 넣어야하는데 보통은 username만 넣는경우가 많다.
return Jwts.builder().setClaims(claims).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + (60000 * 30 * 24)))
.signWith(key).compact();
그 후 return으로 이러한 식으로 넣어주면 되는데
살펴보자면
- Jwts.builder(): JWT 라이브러리의 빌더 클래스를 초기화합니다. 이 빌더는 JWT를 생성하는 데 사용된다.
- .setClaims(claims): JWT의 'claims' 섹션을 설정합니다. 'claims'는 토큰이 전달하는 정보를 담고 있으며, 이 경우에는 이전에 생성된 claims 객체가 사용된다. 이 객체는 사용자의 ID, 역할, 권한 등의 정보를 포함할 수 있다.
- .setIssuedAt(new Date(System.currentTimeMillis())): JWT가 발급된 시간을 설정한다. 여기서는 현재 시간을 발급 시간으로 사용한다.
- .setExpiration(new Date(System.currentTimeMillis() + (60000 * 30 * 24))): 토큰의 만료 시간을 설정한다. 이 예에서는 현재 시간으로부터 60000밀리초(1분) * 30 * 24를 더하여, 대략 30일 후에 만료되도록 설정한다.
- .signWith(key): JWT를 서명하는 데 사용되는 키를 지정한다. 서명은 토큰의 무결성과 인증을 보장하는 데 중요합니다. key는 보안을 위해 비밀로 유지되어야 하는 서버의 개인 키.
- .compact(): 위의 모든 설정을 포함하는 JWT를 생성하고, 이를 문자열 형태로 압축하여 반환한다.
if(byUsername != null) {
response.addHeader("Authorization","Bearer "+jwtToken);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
}
그 후 repository를 통하여 findByUsername을 찾고 디비에 정보가 저장되어있다면
response헤더에 Bearer jwtToken을 만든다.
그 후 json타입과 , UTF-8타입으로 response헤더에 넣는다.

포스트맨으로 로그인 시 헤더부분에 jwt토큰이 생성된걸 알 수 있다.
일단 우리는 jwt토큰까지 만들었고
이 jwt가 유효한지 , 만료된지 안된지 이런걸 확인하는 필터를 또 만들어줘야 한다.
[2장 jwt 토큰 검사 방법]
이제 우리는 토큰까지 만들었고 이제부턴 jwt토큰을 일일이 검증해줘야 한다.
토큰 검증방식은 조금 이해를 할 필요가 있는데 , 일단 기본적인 설명부터 하자면
1. 로컬 스토리지에 있는 jwt토큰을 요청한다.
2. 서버가 jwt토큰을 받고 복호화 된 값을 평문으로 푼다.
3.푼 아이디를 기반으로 데이터베이스에 있는 유저 정보를 찾는다.
4.있다면 해당 유저는 정상적으로 jwt토큰을 가지고 온 유저임으로 다음 필터체인으로 넘긴다
5.만약 jwt토큰이 서명 위조나 jwt토큰을 가지고 오지 않을 시 401에러 혹은 403 에러를 터뜨려서 유저의 접근
권한을 막는다.
package com.example.springsecurity04.Filter;
import com.example.springsecurity04.Dto.UserDto;
import com.example.springsecurity04.JwtUtil.JsonWebToken;
import com.example.springsecurity04.Repository.UserRepository;
import com.example.springsecurity04.Service.UserDetailsInformation;
import com.example.springsecurity04.Table.UserEntity;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
@RequiredArgsConstructor
public class JwtCheckFilter extends OncePerRequestFilter {
private final UserRepository repository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = null;
authorization = request.getHeader("Authorization");
if(!request.getRequestURI().startsWith("/user/") && !request.getRequestURI().startsWith("/admin/")
&&!request.getRequestURI().startsWith("/manager/")) {
filterChain.doFilter(request,response);
return;
}
if(authorization == null || !authorization.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
try {
String token = authorization.split(" ")[1];
Claims username = JsonWebToken.getUsername(token);
String findUsername = username.get("username", String.class);
Optional<UserEntity> userEntityByUsername = repository.findUserEntityByUsername(findUsername);
if(userEntityByUsername.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
ModelMapper mapper = new ModelMapper();
UserDto map = mapper.map(userEntityByUsername.get(), UserDto.class);
UserDetailsInformation information = new UserDetailsInformation(map);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(information,information.getPassword(),information.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
}catch (Exception e) {
if(e instanceof io.jsonwebtoken.security.SignatureException) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
}
이 부분이 jwt 토큰을 체크하는 로직인데 하나하나 잘 봐야한다.
String authorization = null;
authorization = request.getHeader("Authorization");
1. requestHeader에 있는 Authorization의 헤더의 값을 가져와 authorzation 변수에 저장한다.
if(!request.getRequestURI().startsWith("/user/") && !request.getRequestURI().startsWith("/admin/")
&&!request.getRequestURI().startsWith("/manager/")) {
filterChain.doFilter(request,response);
return;
}
2. 요청 값이 "/user/"가 아니거나 /admin아니면 (즉 보호된 리소스가 아니면)
로그인된 유저가 아니기 때문에 토큰이 없다.
그렇기 때문에 다음 필터에 넘기고 해당 클래스를 검증할 필요가 없기 때문에
filterChain으로 넘기고 이 클래스를 종료한다.
이렇게 하기 싫으면
security.exceptionHandling(handler -> handler.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
}
}));
Config 부분에 이러한 핸들러를 추가 해줘도 상관없다.
이 AcessDeinedHanlder의 경우 인가의 실패 했을 때 이 핸들러가 호출되는데 이 핸들러에서
로직을 처리해도 동일한 결과가 나오게 된다.
filterChain.doFilter(request,response);
이 필터체인 부분이 굉장히 중요한데
이 필터체인을 호출한다는 것은 결국 다음 필터체인 -> 즉 정상적으로 이 필터체인이 돌아갔음을 의미한다.
즉 filterChain.doFilter를 호출 했다는 의미는 다음 필터로 넘긴다는 것이고
결국은 이 필터로 들어가면 다음 해당권한을 검사하게 된다.
결국은 이 두필터로 넘긴다는 것은 검증하는 클래스에서 문제가 없음을 나타내고
해당 권한을 넘기게 되어 결국 200ok가 나오게 된다.
if(authorization == null || !authorization.startsWith("Bearer ")){
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
만약 어떤 유저가 jwt토큰이 없는 채로 서버에 접속하거나 , Bearer 토큰(jwt 토큰이) 아니라면
401에러를 터뜨리고 다음 필터체인으로 넘기지 않고 이 클래스를 종료시킨다.
String token = authorization.split(" ")[1];
Claims username = JsonWebToken.getUsername(token);
String findUsername = username.get("username", String.class);
Optional<UserEntity> userEntityByUsername = repository.findUserEntityByUsername(findUsername);
1. 여기서 authorzation 토큰을 Bearer를 제거하고 token에 담는다.
2. 우리가 만든 JwtUtil클래스에 getUsername()이라는 메서드를 만들어
토큰의 복호화된 값을 평문화를 시켜 username에 담는다.
3. 여기서 만약 토큰이 잘못되었다면 서명예외를 터뜨린다.
4.토큰이 정상적이라면 username의 객체는 존재하며
claims 객체는 맵형식으로 되어있다고 생각하면 쉽다.
즉 우리가 처음에 키 밸류로 넣었던
clamis.put("username",username);의 값을 꺼내야 한다.
public static Claims getUsername(String token){
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
getUsername을 꺼내는 로직이다.
String findUsername = username.get("username", String.class);
이렇게 String.Class타입으로 꺼내 findUsername에 담는다.
Optional<UserEntity> userEntityByUsername = repository.findUserEntityByUsername(findUsername);
그 후 DB에 저장되어있는 유저의 정보를 가져온다.
if(userEntityByUsername.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
또한 디비에 있는 값이 존재하지 않을때는 사용자가 없기때문에 401에러를 주고 클래스 종료
ModelMapper mapper = new ModelMapper();
UserDto map = mapper.map(userEntityByUsername.get(), UserDto.class);
객체가 있다면 ModelMapper로 변환 후 map에 저장.
UserDetailsInformation information = new UserDetailsInformation(map);
그 후 우리가 유저 세부정보를 넣었던 객체를 생성해서 생성자에 넣는다.
이유(UsernamePassWordAuthenticationToken의 객체를 만들어서 SecurityContextHolder에 넣어야 함)
그런데 이 토큰 객체를 만들기 위해선 유저 세부정보에 있는 저장 정보를 넣어야함.
package com.example.springsecurity04.Service;
import com.example.springsecurity04.Dto.UserDto;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
@Data
public class UserDetailsInformation implements UserDetails {
private final UserDto dto;
public UserDetailsInformation(UserDto dto) {
this.dto = dto;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(() -> {
return dto.getRole();
});
return authorities;
}
@Override
public String getPassword() {
return dto.getPassword();
}
@Override
public String getUsername() {
return dto.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;
}
}
유저의 세부 정보를 저장하는 클래스의 객체를 생성한다.
(생성하는 이유는 정상적으로 인증이 완료되었다면 UsernamePassWordAuthenticationToken에 값을 넣고
true(인증됨)으로 바꿔야 하기 때문이다.
UsernamePasswordAuthenticationToken userToken =
new UsernamePasswordAuthenticationToken(context,context.getPassword(),context.getAuthorities());
UsernamePassWordAuthentcationToken 객체를 만들어서 생성자에 넣어줘야 하는데

이 객체는 두 개의 생성자를 가지고 있다.
넣는방식에 따라 fasle true를 반환하는데
우리는 true(인증됨)이 되어야하기 때문에 두번째 생성자의 매개변수에 넣어야한다.
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(information,information.getPassword(),information.getAuthorities());
그렇기 때문에 context 객체 , 비밀번호 , 권한 정보를 넣는다.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
여기가 가장 중요한데 SecurityContextHolder는 매 요청마다 반드시 삭제된다.
스프링 시큐리티는 세션에 SecurityContextHolder를 저장해서
세션에서 SecurityContextHolder가 있는지 검사를 해서 유저가 인증된 유저인지 아닌지 검증하는데
우리는 세션방식을 껏기 때문에 SecurityContextHolder가 매 요청마다 반드시 삭제됨으로
만약 jwt토큰을 가져오고 정상적으로 요청이 되었다면 매 요청마다 반드시 SecurityContextHolder를 만들어줘야 한다.
filterChain.doFilter(request,response);
이렇게 정상적으로 완료 되었다면 다음 체인필터로 넘긴다.
}catch (Exception e) {
if(e instanceof io.jsonwebtoken.security.SignatureException) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
그리고 이부분은 예외처리 부분인데
만약 서명 에러가 터졌을 경우 서명이 다름으로 401에러를 준다.
dnjswns1992/jwt at git-Woo1 (github.com)
GitHub - dnjswns1992/jwt: jwtdocument
jwtdocument. Contribute to dnjswns1992/jwt development by creating an account on GitHub.
github.com
프로젝트 깃허브 주소
'스프링 시큐리티 -세션 > Chatper02 스프링 시큐리티 인증 흐름' 카테고리의 다른 글
| 7장 스프링 시큐리티 권한 설정 동작 방식 및 구현 (1) | 2024.01.12 |
|---|---|
| 6장 스프링 시큐리티 Authentication,SecurityContextHolder 인증흐름 (1) | 2023.12.28 |