반응형

스프링 시큐리티 및 JWT 를 활용하기 위해 결국 유저정보를 저장하는 DB 가 있어야 합니다.


아래 글과 겹치는 부분 있을 수 있으니 먼저 확인해보세요. 아래 포스트는 디비 없이 하드코딩으로 user 와 password 를 설정했습니다.
https://juntcom.tistory.com/249 

 

spring security 와 jwt 를 이용한 인증 인가(spring boot3)

• Spring Security는 Spring 기반 애플리케이션의 보안을 제공하는 강력하고, 유연한 프레임워크입니다.특징  • 인증(Authentication)과 인가(Authorization) 기능 제공 • 다양한 보안 공격 방어 (CSRF, XSS 등)

juntcom.tistory.com

 

회원 테이블

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN NOT NULL,
    authority VARCHAR(50) NOT NULL
);

user 테이블과

authority 권한 테이블을 정규화 해서 나눌 수도 있지만 한 유저당 여러권한이 없다고 보는게 낫고 예시를 위해 한 테이블로 진행하려고 합니다. 

 

Entity Class

@Setter
@Getter
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private boolean enabled;

    @Column(nullable = false)
    private String authority;

}

 

 

CustomUserDetailsService 

UserDetailsService 를 implements 해야 합니다. 

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }

        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
                user.isEnabled(), true, true, true,
                Collections.singletonList(new SimpleGrantedAuthority(user.getAuthority())));
    }
}

UserRepository

package com.junt.studybasic.repository;

import com.junt.studybasic.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

 

Security 

package com.junt.studybasic.config;

import com.junt.studybasic.filter.JwtRequestFilter;
import com.junt.studybasic.service.CustomUserDetailsService;
import com.junt.studybasic.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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
public class SecurityConfig {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorizeRequests ->
                authorizeRequests
                    .requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated()
            )
            .sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

Login Controller 

package com.junt.studybasic.controller;

import com.junt.studybasic.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);

        return jwt;
    }
}

class AuthRequest {
    private String username;
    private String password;

    // Getters and Setters
}

 

 

회원가입 API

UserService

package com.junt.studybasic.service;

import com.junt.studybasic.entity.User;
import com.junt.studybasic.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User registerNewUser(String username, String password, String authority) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setEnabled(true);
        user.setAuthority(authority);
        return userRepository.save(user);
    }
}

 

AuthController

 

package com.junt.studybasic.controller;

import com.junt.studybasic.entity.User;
import com.junt.studybasic.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public User registerUser(@RequestBody RegisterRequest registerRequest) {
        return userService.registerNewUser(
                registerRequest.getUsername(),
                registerRequest.getPassword(),
                registerRequest.getAuthority()
        );
    }
}

@Getter
@Setter
class RegisterRequest {
    private String username;
    private String password;
    private String authority;

}

 

 

JwtUtil

@Component
public class JwtUtil {

    private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails) {
        return createToken(userDetails.getUsername());
    }

    private String createToken(String subject) {
        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
                .signWith(SECRET_KEY)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

 

JwtRequestFilter

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {

                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

 

참고

유저 권한에 따라 코드를 다르게 실행하는 방법

1. 메서드 수준의 보안 애노테이션 사용

 

Spring Security는 메서드 수준에서 접근 제어를 지원하는 몇 가지 애노테이션을 제공합니다. 가장 일반적인 애노테이션은 @PreAuthorize@Secured입니다.

1.1 @PreAuthorize 애노테이션 사용

 

@PreAuthorize 애노테이션을 사용하면 메서드 호출 전에 표현식을 평가하여 접근을 제어할 수 있습니다. 이를 위해 @EnableGlobalMethodSecurity(prePostEnabled = true)를 설정해야 합니다.

package com.junt.studybasic.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    // 추가 설정이 필요 없다면 이 클래스는 비어 있어도 됩니다.
}

서비스 클래스 예시

package com.junt.studybasic.service;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public void adminOnlyMethod() {
        // 관리자 전용 메서드 로직
    }

    @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
    public void userOrAdminMethod() {
        // 사용자 또는 관리자 접근 가능 메서드 로직
    }
}

 

2. Spring Security 권한 검사 API 사용

 

Spring Security의 권한 검사 API를 사용하여 프로그램 내에서 동적으로 권한을 검사할 수도 있습니다.

 

2.1 SecurityContextHolder를 사용한 권한 검사

@Service
public class MyService {

    public void performActionBasedOnRole() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
            // 관리자 전용 로직
        } else if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_USER"))) {
            // 사용자 전용 로직
        } else {
            // 기타 권한 로직
        }
    }
}

 

3. 컨트롤러에서 권한에 따라 다른 로직 수행

 

Spring MVC 컨트롤러에서도 @PreAuthorize를 사용할 수 있습니다.

 

3.1 컨트롤러에서의 예시

package com.junt.studybasic.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String adminEndpoint() {
        return "관리자 전용 페이지";
    }

    @GetMapping("/user")
    @PreAuthorize("hasRole('ROLE_USER')")
    public String userEndpoint() {
        return "사용자 전용 페이지";
    }

    @GetMapping("/common")
    @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
    public String commonEndpoint() {
        return "모든 사용자 접근 가능 페이지";
    }
}

 

요약

 

메서드 수준 보안 애노테이션: @PreAuthorize@Secured를 사용하여 메서드 접근 제어.

권한 검사 API: SecurityContextHolder를 사용하여 동적으로 권한 검사.

컨트롤러에서 권한 검사: @PreAuthorize를 사용하여 요청 핸들러 접근 제어.

템플릿 엔진에서 권한 검사: Thymeleaf를 사용하여 동적으로 UI 요소 표시.

 
반응형

+ Recent posts