먼저 jwt에 대해 설명하겠다.
jwt는 Header.Payload.Signature 구조로 이루어져 있다.
Header - JWT임을 명시 & 사용된 암호화 알고리즘
Payload(Claim) - 정보
Signature - 서명으로 토큰의 무결성을 검증하는데 사용된다. 구조는 (BASE64(Header)+BASE64(Payload) + 암호화 키)와 같다.
jwt는 내부 정보를 단순 BASE64 방식으로 인코딩하기 때문에 외부에서 쉽게 디코딩 할 수 있다. 따라서 외부에서 열람해도 되는 정보를 담아야한다.
jwt 암호화 종류
-양방향 : 대칭키 / 비대칭키
-단방향
로직
SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//세션 사용 안하므로 csrf 꺼주면 된다.
.csrf(AbstractHttpConfigurer::disable);
//formLogin을 사용 안 하므로 UsernamePasswordAuthenticationFilter를 커스텀화해야 한다.
//formLogin은 세션을 활용한 방식이므로 jwt 사용할 때는 필요없음
http
.formLogin(AbstractHttpConfigurer::disable);
//jwt 인증 사용하므로 http basic 인증 방식 사용 X basic 인증 방식이란 헤더에 username과 password을 암호화하여 인증하는 것을 말한다.
http
.httpBasic(AbstractHttpConfigurer::disable);
http
.authorizeHttpRequests((auth)->auth
.requestMatchers("/login", "/", "/join").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());
http // UsernamePasswordAuthenticationFilter자리에 LoginFilter 넣음 / authenticationManager()함수로 인자 전달
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration),jwtUtil), UsernamePasswordAuthenticationFilter.class);
http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
//jwt는 세션을 stateless로 관리. stateless란 서버가 클라이언트의 상태를 보존하지 않는 것으로 서버 무한 확장이 가능해진다.
http
.sessionManagement((session)-> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
MainController.java
@Controller
@ResponseBody
public class MainController {
@GetMapping("/")
public String mainP(){
String name = SecurityContextHolder.getContext().getAuthentication().getName();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority grantedAuthority = iterator.next();
String authority = grantedAuthority.getAuthority();
return "Main Controller" + name + authority;
}
}
CustomUserDetails.java
public class CustomUserDetails implements UserDetails {
private final UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
//SimpleGrantedAuthority에 들어갈 ROLE의 형태는 ROLE_이라는 prefix가 있어야 한다.
authorities.add(new SimpleGrantedAuthority(userEntity.getRole()));
return authorities;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.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;
}
}
JWTFilter.java
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 {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer ")) {
System.out.println("token null");
// 다음 필터로 넘김
filterChain.doFilter(request, response);
return; // 리턴 필수
}
String token = authorization.substring("Bearer ".length());
if(jwtUtil.isExpired(token)){
System.out.println("token expired");
filterChain.doFilter(request, response);
return; // 리턴 필수
}
String username = jwtUtil.getUserName(token);
String role = jwtUtil.getRole(token);
UserEntity userEntity = new UserEntity();
userEntity.setUsername(username);
userEntity.setRole(role);
//비번은 안 필요함
CustomUserDetails customUserDetails = new CustomUserDetails(userEntity);
//이미 로그인 되어있는 상태이므로 LoginFilter 안 거침 따라서 attemptAuthentication() 대신하여 Authentication 생성
//Authentication 객체를 통해 권한 확인 / 이는 세션의 한 종류이지만 응답이 완료되면 바로 삭제됨
Authentication authentication = new UsernamePasswordAuthenticationToken(customUserDetails,null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
JWTUtil.java
@Component
public class JWTUtil {
private SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String secret){
//properties에 있는 jwt secret 키를 HS256알고리즘으로 암호화
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
public String getUserName(String token){
//verifyWith()함수로 token이 이 서버에서 만들었는지 확인
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username",String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public Boolean isExpired(String token) {
//현재 시간 기준으로 before이면 true 반환
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
public String createToken(String username, String role, Long expiredMs){
//jwt token 만들어서 반환
return Jwts.builder()
.claim("username", username) // Payload에 값 저장하는 함수
.claim("role",role)
.issuedAt(new Date(System.currentTimeMillis())) // 토큰이 만들어진 시간
.expiration(new Date(System.currentTimeMillis()+expiredMs)) // 토큰이 만료되는 시간
.signWith(secretKey) // secretKey 이용해 signature 생성
.compact();
}
}
LoginFilter.java
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final JWTUtil jwtUtil;
private final AuthenticationManager authenticationManager;
//security config에서 인자로 넘어오므로 @RequiredArgsConstructor 사용 X
public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
@Override
//공부자료 05에 있는 Authentication 생성하는 함수
//AbstractAuthenticationProcessingFilter에서 검증을 하기 위한 메소드 구현
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//클라이언트 요청에서 username, password 추출해주는 함수
String username = obtainUsername(request);
String password = obtainPassword(request);
System.out.println(username);
//AuthenticationManager에서 검증을 하려면 token에 담아야 함
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password, null);
//token을 AuthenticationManager에게 전달
//반환값엔 token에 대한 정보와 UserDetail을 구현한 클래스의 정보가 들어있다.
return authenticationManager.authenticate(authRequest);
}
//로그인 실패시 실행되는 메서드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setStatus(401);
}
//로그인 성공시 실행되는 메서드 여기서 jwt 발급하면 됨
@Override //UsernamePasswordAuthenticationFilter가 반환한 객체
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//UserDetail 반환하는 함수
CustomUserDetails customUserDetails = (CustomUserDetails) authResult.getPrincipal();
String username = authResult.getName();
//UserDetail을 구현한 클래스의 함수 호출
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority grantedAuthority = iterator.next();
String role = grantedAuthority.getAuthority();
String token = jwtUtil.createToken(username, role, 60*60*10L);
response.addHeader("Authorization", "Bearer " + token);
}
}
CustomUserDetailsService.java
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userData = userRepository.findByUsername(username);
if(userData != null) {
return new CustomUserDetails(userData);
}
return null;
}
}
'Spring Boot > Security' 카테고리의 다른 글
Spring Boot - 프론트에서 OAuth2.0 책임 다 지을 때 (0) | 2024.04.18 |
---|---|
Spring Boot - jwt 다중 토큰 사용하기 (0) | 2024.04.18 |
Spring Boot - 계층 권한 ( Role Hierarchy ) (0) | 2024.04.13 |
Spring Boot - JWT와 OAuth2 사용하여 로그인하기 (0) | 2024.04.12 |
Spring Boot - spring security + oauth2 (0) | 2024.04.11 |