본문 바로가기

개발기술/Spring

Spring Security - JWT 인증방식 구현

스프링 Security - Jwt Authentication 구현 흐름

  SignUp 시, 사용자는 사용자 이름(username), 비밀번호(password) 등의 정보를 제공하며, 이러한 정보는 UserDetails 객체(또는 유사한 엔티티)에 캡슐화되어 데이터베이스에 저장됩니다. 비밀번호는 안전하게 해시된 후 저장됩니다.

 SignIn시, 입력된 자격 증명(사용자 이름과 비밀번호)은 저장된 UserDetails와 비교됩니다. 비밀번호가 저장된 해시와 일치하면, JWT 토큰이 생성되어 사용자에게 반환됩니다. 이 토큰에는 사용자의 ID나 권한 등의 정보가 인코딩되어 있습니다.

  사용자가 보호된 API에 접근하면, 필터(e.g., SecurityContextPersistenceFilter)가 요청을 가로채고 SecurityContext에 인증 객체가 있는지 확인합니다. 만약 인증 객체가 없다면, JWT토큰을 추출하는 필터로 (e.g OncePerRequestFilter) 요청을 넘기고 해당 필터에서 JWT 토큰을 추출하여 서명과 만료 시간을 포함한 유효성을 검사합니다. 토큰이 유효하다면, 이를 기반으로 Authentication 객체(e.g., UsernamePasswordAuthenticationToken)가 생성되고 SecurityContext에 저장됩니다.

  이후 동일한 세션 내에서 서버는 SecurityContext에 저장된 인증 객체를 신뢰하여 JWT 토큰을 다시 확인하지 않고 요청을 처리하게 됩니다. 또한, 구현 방식에 따라 JWT 토큰은 만료 시간이 있을 수 있으며, 클라이언트는 토큰이 만료되면 갱신하거나 재인증을 해야 할 필요가 있습니다.

 

  1. User Sign-Up:
    • During registration, the user provides details such as username, password, etc.
    • These details are encapsulated in a UserDetails object (or an equivalent entity) and saved to the database.
    • The password is securely hashed before storage.
  2. User Sign-In (Authentication):
    • When a user attempts to sign in, their credentials (username and password) are compared to the stored UserDetails.
    • If the password matches the stored hash, a JWT token is generated and returned to the user. This token contains encoded information, such as the user's ID or roles.
  3. JWT Token Interception & Validation:
    • When the user accesses protected APIs, a filter (e.g., OncePerRequestFilter) intercepts the request.
    • The filter checks if the SecurityContext holds an authentication object. If it doesn't, it inspects the JWT token.
    • The JWT token is extracted (usually from the request header), validated (signature check, expiration, etc.), and if valid, an authentication object (e.g., UsernamePasswordAuthenticationToken) is created based on the information in the token.
  4. Security Context:
    • The authentication object is stored in the SecurityContext.
    • For subsequent requests within the same session, the server does not recheck the JWT token if the SecurityContext already contains the authentication object. Instead, it trusts the security context and allows access accordingly.
  5. Token Expiry & Refresh:
    • Depending on your implementation, tokens may have an expiration time. The client would need to refresh the token when it expires or re-authenticate.

Security 구현하기 :  Dependency

Jjwt, SpringSecurity 의존성 추가하기

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

Security 구현하기 :  UserDetails

- Credential(ID,PW)를 보유하고 있는 UserDetail을 구현한다. UserDetail은 일반적으로 UserEntity를 Input으로 받아서 생성됨.

UserDetail이 구현되어야 Authentication 과정에서 User의 Credential을 검증하고 후에 Authority를 검증하는 과정이 가능함.

@Entity
@Table(name = "users")
public class UserEntity {

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

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

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private boolean enabled;

    @Column(nullable = false)
    private boolean accountNonExpired;

    @Column(nullable = false)
    private boolean credentialsNonExpired;

    @Column(nullable = false)
    private boolean accountNonLocked;

 

- UserDetails 인터페이스는  isexpired, islocked 등 여러가지 필드값과  Authority객체인 GrantedAuthority 인터페이스를 값으로 갖게한다.  GrantedAuthority 인터페이스는 roles String을 -> Authority 구현체들("Role_"read/write... 형식)으로 변환되게끔하여  Spring 기능 (method annotation @hasRole() 등)에 활용될 수 있도록 함. (SimpleGrantedAuthoritie는 roles -> GrantedAuthority의 가장 기본적인 형태임)

@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDetails implements UserDetails {

    private Long id;
    private String username;
    private String password;
    private List<String> roles;


    public static MemberDetails fromEntity(MemberEntity memberEntity) {
        return MemberDetails.builder()
                .id(memberEntity.getId())
                .username(memberEntity.getUsername())
                .password(memberEntity.getPassword())
                .roles(memberEntity.getRoles())
                .build();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }

 

- Authority Enum으로 저장

public enum Authority {
    ROLE_READ,
    ROLE_WRITE;
}

Security 구현하기 :  UserService

UserDetail이 구현이 완료되었다면, Persistency계층과 Userdetail을 연결시키는 UserDetailService를 구현한다. UserDetailService는 1. UserDetail 저장(Signin) 2. UserDetail 대조하여 호출 3. UserDetail 호출

 

/*
유저 디테일을 불러오고 저장하는 클래스
 */
public class MemberAuthService implements UserDetailsService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;


    /*
    유저로부터 Id와 Pw를 받아서 Id중복여부를 확인한 후
    pw를 encoding하여 db에 저장함
     */
    @Transactional
    public void register(String memberId, String password){
        if(memberRepository.existsByMemberId(memberId)){
            throw new UsernameNotFoundException("Member already exists");
        }

        memberRepository.save(
                MemberDetails.builder()
                        .memberId(memberId)
                        .password(passwordEncoder.encode(password))
                        .build());
    }

    public UserDetails authenticate(String memberId, String password ){
        UserDetails userDetails = this.loadUserByUsername(memberId);

        if(!passwordEncoder.matches(password, userDetails.getPassword())){
            throw new BadCredentialsException("Wrong password");
        }
        return userDetails;
    }
    

    @Override
    public  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return memberRepository.findByMemberId(username).orElseThrow(() -> new UsernameNotFoundException("Member does not exist"));
    }
}

 

- Appconfig에서 PasswordEncoder Bean을 등록한다. 이때, Spring에서 제공하는 BcrptPasswordEncoder을 사용하여 주입한다.

@Configuration
public class AppConfig {
    @Bean
    public Trie<String,String> tire(){
        return new PatriciaTrie<>();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
passwordEncoder.encode(password)
passwordEncoder.matches(password, memberEntity.getPassword())

 

  • The matches method takes the raw password and the encoded password (the one stored in the database).
  • It hashes the raw password using the same algorithm and compares it to the stored hash.

Security 구현하기 :  Sign Controller

- Sign-up에는 아이디 중복여부를 체크하고 Persistency에 저장

- Sign-in에는 DaoAuthenticate 후에 JWT를 return

public class AuthController {

    private final MemberAuthService memberAuthService;
    private final JwtHandler jwtHandler;
    private final UserService userService;
    private final PartnerService partnerService;
    private final PasswordEncoder passwordEncoder;


    /*
    memberEntity 와 partnerEntity 를 최초 생성함.
    단, memberEntity 는 관련기능은 후속추가되어 클래스 추가관리되었고 @Transactional 관리를 위해
     controller 에서 서비스 호출하지 않고 partnerService 내에서 관련로직 호출함

     */

    @PostMapping("/partners")
    public ResponseEntity<CreatePartner.Response> createPartner(
            @RequestBody @Valid CreatePartner.Request request) {
        log.info("Creating partner : {}", request);

        PartnerDto partnerDto = partnerService.createPartner
                (       request.getPartnerId(),
                        request.getPassword(),
                        request.getPartnerName(),
                        request.getBusinessId(),
                        request.getPhoneNumber()
                );

        return ResponseEntity.ok().body(
                CreatePartner.Response.fromDto(partnerDto));
    }


    /*
        memberEntity 와 userEntity 최초 생성함.
        단, memberEntity 는 관련기능은 후속추가되어  클래스 추가관리되었고 @Transactional 관리를 위해
        controller 에서 서비스 호출하지 않고 userService 내에서 관련로직 호출함
     */

    @PostMapping("/users")
    public ResponseEntity<CreateUser.Response> createUser(
            @RequestBody @Valid CreateUser.Request request) {

        log.info("Creating user request received : {}", request.getUserId());

        UserDto userDto = userService.createUser(
                request.getUserId(),
                request.getPassword(),
                request.getUserName(),
                request.getPhoneNumber()

        );

        return ResponseEntity.ok().body(
                CreateUser.Response.fromDto(userDto));
    }


    /*
    Dao Authentication 을 행하고 결과값으로 JWT token 을 받음
     */
    @PostMapping("/signIn")
    public ResponseEntity<String> signIn(@RequestBody @Validated SignAuth.SignIn signIn) {

        UserDetails userDetails = memberAuthService.authenticate(signIn.getId(), signIn.getPassword());

        String token = jwtHandler.generateToken(userDetails.getUsername(), userDetails.getAuthorities());

        return ResponseEntity.ok(token);
    }

 

- Request 객체만들기

public class Auth {

    @Data
    public static class SignInRequest {
        private String userName;
        private String password;

    }

    @Data
    public static class SignUpRequest {
        private String userName;
        private String password;
        private List<String> roles;
    }
}

Security 구현하기 :  Jwts dependency 알아보기

JWT(Json Web Token)

  사용자 인증 및 식별에 사용되는 JSON 기반의 토큰입니다. JWT는 Header, Payload,Signature 3부분으로 나누어진다. JWT는 서버에서 세션을 관리하지 않고, 클라이언트 측에 토큰을 전달하여 사용자를 인증하는 방식으로 작동합니다.

  실무적으로 DAO Authentication을 통해서 인증이 완료되면 Dao로부터 꺼낸 Authority, Principal, Credential에 대한 정보를 JWT에 담아서 인증,권한 확인 등에 사용한다.

 

JWT encoding 방식

  JWT는 Base64 Encoded된 것으로 엄밀히 말하면 암호화되지 않고 정보를 담은 것입니다. 암호화된 부분은 Signature부분으로 Header과 Payload의 값이 변경되면 Signature이 변경되어 컨텐츠의 유효성을 확인할 수 있다. 

base64 : Base64 encoding takes three bytes (3*8 bits) of binary data and splits them into (4*6bits) groups. Each of these 6-bit groups is then represented by a single ASCII character. 

 

JWT 구조

{
        "alg":"HS512",
        "typ":"JWT"
}
  • Header : 1. 토큰의 타입(JWT) 2. 어떤 암호화 알고리즘이 적용되었는지, 두가지 정보가 저장
{
        "sub": "1234567890",
        "name": "John Doe",
        "admin": true,
        "roles": ["ROLE_USER", "ROLE_ADMIN"]
}
  • Payload :
    • 1. 등록된 클레임(Registered Claims): 토큰의 만료 시간(exp), 발급자(iss), 수신자(aud), 발급 시간(iat) 등이 포함
    • 2. 공개 클레임(Public Claims): 사용자 정의 클레임으로, 사용자 ID, 이메일, 사용자 이름 등의 정보를 담을 수 있습니다.
    • 3. 비공개 클레임(Private Claims): 서버와 클라이언트 간에 동의된 비공개 데이터입니다.
  • 서명 : JWT의 무결성을 보장하기 위해 사용됩니다. 서버는 헤더 + 페이로드+ secretkey + al를 조합하여, 지정된 암호화 알고리즘을 사용해 서명을 생성합니다.

Jwts코드 예시

- Jwts 코드예시 : Jwt token 생성을 위해서 Header, Payload, Sign 방식을 설정한다. 

// Secret key in string format
String secretKeyString = "mySecretKey"; // This should be securely stored

// Convert the secret key string to a SecretKey object
SecretKey secretKey = new SecretKeySpec(secretKeyString.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());

// Build and sign the JWT
String jwt = Jwts.builder()
        .setHeaderParam("typ", "JWT")
        .setSubject("user123")
        .setIssuer("my-app")
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour expiration
        .claim("role", "admin") // Custom claim
        .signWith(secretKey, Signature

 

대표 매소드 정리

Jwts.builder(): 

  • setHeaderParam(String name, Object value) : Sets a single custom header parameter.
  • setSubject(String subject) : Sets the subject (sub) claim, typically used to represent the user or entity the token is issued for.
  • setClaims(Map<String, Object> claims)  : Sets multiple claims(predefined) in the JWT. Claims are the payload data that include information like the subject, issuer, expiration time, etc.
    •  it replaces the entire set of claims that have been accumulated so far, including any claims you may have set earlier in the builder chain, such as setSubject, setExpiration, or setIssuedAt.
  • claim(String name, Object value) : Sets a single claim(usualy customized) in the JWT.
  • setIssuer(String issuer) : Sets the issuer (iss) claim, which identifies the principal that issued the JWT.
  • setIssuedAt(Date issuedAt) : Sets the issued-at (iat) claim, which indicates the time at which the JWT was issued.
  • setExpiration(Date expiration) : Sets the expiration (exp) claim, which indicates the time after which the JWT should no longer be accepted.
  • signWith(Key key, SignatureAlgorithm alg) : to digitally sign a JWT using a specified cryptographic key and signature algorithm. 
  • compact() : Finalizes the construction of the JWT and returns it as a String. This method must be called at the end of the JWT building process
 

 

// Convert the secret key string to a SecretKey object
SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS256.getJcaName());

// Recommended method:
String jwt = Jwts.builder()
        .setSubject("user123")
        .signWith(secretKey, SignatureAlgorithm.HS256)
        .compact();

 

SecetKey

이전에는 .signWith(SignatureAlgorithm.HS512, String secretkey)와 같은 방식으로 secretkey가 String으로 입력되었다면 이제는 client 측에서 string을 byte[]로 변환한 후 Secretkey라는 객체를 별도로 만들고 signwith에 그 객체를 input 하도록 되어있다. 이 변화의 트렌드는 moving away from "magic" or automatic behavior that might lead to mistakes and instead encouraging explicit, well-understood actions that align with security best practices 라고 한다.

 

new SecretKeySpec(byte[], Algorithm Name:): Creates a new SecretKeySpec object, which implements the SecretKey interface. This object is used for cryptographic operations such as signing or verifying a JWT.

 

  • Byte Array: The first parameter is the byte array of the secret key.
  • Algorithm Name: The second parameter is the algorithm's name (e.g., "HmacSHA512"), which specifies how the key will be used.

 

secretKey.getBytes(StandardCharsets.UTF_8):

  • Purpose: Converts the secretKey string into a byte array using the UTF-8 character encoding.

 

Claims claims = Jwts.claims().setSubject("user123");
claims.put("role", "admin");

String jwt = Jwts.builder()
        .setClaims(claims)
        .signWith(SignatureAlgorithm.HS256, secretKey)
        .compact();

 

Jwts.claims():

This method creates a new, empty Claims object. Claims is essentially a Map<String, Object> that holds the key-value pairs representing the claims in the JWT's payload. This Claims object is where you define both standard and custom claims that will be included in the JWT.

  • claimsObject.put(KEY_ROLES, role): The put method is used to add a custom claim to the Claims object.
  • claimsObject.setSubject(username):
    • It sets the sub claim within the Claims object, which you can then pass to JwtBuilder using setClaims() to include it in the JWT.

Jwts Parser : Jwts.parseClaimsJws

parsing하면서 sign의 validation을 같이 확인하는 기능이 포함되어 있음

Claims claims = Jwts.parserBuilder()
        .setSigningKey(key)
        .build()
        .parseClaimsJws(token)
        .getBody();
  • Jwts.parserBuilder(): This initializes a new JwtParserBuilder, which is used to create a JwtParser that can parse and validate JWTs.
  • .setSigningKey(key): Specifies the cryptographic key that should be used to validate the signature of the JWT.
  • .build() : Finalizes the construction of the JwtParser based on the configurations provided (like the signing key).
  • .parseClaimsJws(token):  Parses the JWT, which is expected to be in the JWS (JSON Web Signature) format. This method performs the following steps:
  • .getBody():  Retrieves the claims (i.e., the payload) from the parsed and validated JWT.

 

Security 구현하기 :  JwtTokenProvider(using Jwts) 

-  JWT를 생성할때는 jsonwebtoken jjwt library class를 사용하여 build와 parsing을 통해서 진행한다. 

- userEntity로부터 DAO authentication이 진행되면, userdetail의 정보(username, roles)와 만든시간, 유효시간, secretkey 등을 조합하여 JWT을 만들어 낸다

- 그리고 Token이 Client로부터 발신되었을 때 주요 정보(claim, client)를 추출해내기위한 utility method를 생성한다.

- secretkey는 특별한 문자를 만들어내기위해서 CLI 'echo plaintext | base64'를 사용한다. 암호화키는 주로 base64 으로 표기하여 유출을 방지한다. JWT생성을 위해서 입력될때는 decoded되어서 원본형태로 입력된후 jwt 방식으로 다시 encode된다.

 

jwt:
  secret: c2FuZ2h3YS1zcHJpbmdzZWN1cml0eS1kaXZpZGVuLXByYWN0aWNlLWp3dC1zZWNyZXRrZXkK

 

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
    private static final String KEY_ROLES = "roles";
    private static final Long TIME_TOKEN_EXPIRE_TIME = 1000 * 60 * 60L;//1hour
    private final MemberService memberService;
    @Value("${spring.jwt.secret}")
    private String secretKey;

    // DAO Authentication을 진행한 후, entity의 username(principal)과 roles
    // (authorities)를 JWT에 담아내어 반환한다.
    public String generateToken(String username, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put(KEY_ROLES, roles);

        var now = new Date();
        var expiredTimes = new Date(now.getTime() + TIME_TOKEN_EXPIRE_TIME);

        SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS512.getJcaName());


        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiredTimes)
                .signWith(key, SignatureAlgorithm.HS512)  // Using SecretKeySpec instead of a raw string
                .compact();
    }


    public String getUsernameFromToken(String token) {
        Claims claims = this.parseClaimsFromToken(token);
        return claims.getSubject();
    }

    private Claims parseClaimsFromToken(String token) {

        SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), SignatureAlgorithm.HS512.getJcaName());


        try {
            return Jwts.parserBuilder().setSigningKey(key).build()
                    .parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException e) {
            return e.getClaims();
        }
    }

 

 

- 그 외 Client가 token을 발송했을때, filter layer에서시간적으로 여전히 유효한지 확인하고, 유효하다면 Token이  authentication을 생성하여 SecurityContext에 input하기 위한 함수들을 생성한다.

 

UsernamePasswordAuthenticationToken:

  • This is a concrete implementation of the Authentication interface provided by Spring Security. It represents an authentication request that contains the user’s principal (userDetails), credentials (password), and authorities (roles/permissions).

 

Security 구현하기 :  JWT Authentication Filter

JWT Authentication을 구현할때 주로 사용되는 필터는 두가지로 아래와 같다. 우선 SecurityContextPersistenceFilter는 Thread가 SecurityContext를 보유하고 있는지 확인하고 미보유시 JwtAuthentificationFilter로 request를 넘긴다. JwtAuthentificationFilter는 해당 request가 1. jwt토큰을 보유하고 있는지 2. 토큰이 유효한지 두가지를 확인하여 맞다면 SecurityContext에 Authentication객체를 삽입한다.

 

 

FilterStack

  • The SecurityContextPersistenceFilter : one of the first filters in the Spring Security filter chain. Its primary job is to manage the SecurityContext for each request.
  • JWT Authentication Filter: a custom filter or Spring Security's built-in filter would typically check for the presence of a JWT in the request . The filter extracts the token, validates it, and, if valid, creates an Authentication object and stores it in the SecurityContext.
  • UsernamePasswordAuthenticationFilter : a built-in filter in Spring Security that handles authentication requests involving username and password. It is part of the default security filter chain and is typically responsible for processing login forms (e.g., /login requests). Here’s how it works:
    •  

 

스프링 Security에서 필터를 구현할때, OncePerRequestFilter 주로 사용하는데, 해당 필터는 request 1개당 1회만 실행되어 Security 구현에 적합하다 

// token을  포함되어있는지 파싱하여 확인하는 작업을 구현
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    public static final String TOKEN_HEADER = "Authorization"; // Http header
    public static final String TOKEN_PREFIX = "Bearer "; // jwt Token에 처음붙음

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = resolveTokenFromRequest(request);

        if(StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
            Authentication auth = jwtTokenProvider.getJwtAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }

    private String resolveTokenFromRequest(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_HEADER);

        if(!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) {
            return token.substring(TOKEN_PREFIX.length());
        }

        return null;
    }
}

 

  • PreAuthorize and FilterSecurityInterceptor: For method-level security (@PreAuthorize, @Secured), the security expressions check the SecurityContext to see if the current user has the necessary permissions.

스프링 SecuirtyConfiguration

Filter를 어떻게 적용할지에 대해서 세부조정을 하는 클래스. 우선,  Custom구현한 JWT Authentification Filter를 Chain of Filter 내에서 몇번째 순서에 배치하는지 확정하고 더불어 해당 Filter를 발동시킬 Url조건과 예외처리를 할 Url 등을 지정한다. Signup page등은 Filter 발동에서 제외가 필요하다.

 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;


    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth.requestMatchers(
                        "/**/signIn").permitAll())
                .authorizeHttpRequests(auth -> auth.requestMatchers("/css/**"
                        ,"/js/**", "/images","/h2-console/**", "/fonts/**",
                        "/images/**", "/favicon.ico").permitAll())

                .addFilterBefore(jwtAuthFilter,
                        UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
  • @EnableWebSecurity annotation  : disable the default auto-configuration and take full control of your application's security configuration. This allows you to customize security rules, authentication mechanisms, and more.

 

  • SecurityFilterChain Bean:  For Integration with Spring's Dependency Injection :  SecurityFilterChain은 FilterChainProxy라는 스프링 내부코드 객체에서 사용한다. default설정이 아닌, 개발자 설정을 원할때 자체적으로 빈으로 등록하여 커스터마이징하는 것.

 

 

  • HttpSecurity:a configuration object that allows you to define how the security filter chain should behave. Once configured, the HttpSecurity object is used to build the SecurityFilterChain
    • .csrf(csrf -> csrf.disable()): Disables Cross-Site Request Forgery protection, which is often disabled in stateless applications (like those using JWT).
    • sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)): Configures the session management to be stateless, meaning no session will be created or used by Spring Security, which is typical for JWT-based authentication.
    • authorizeHttpRequests: Configures URL-based security.
      • .requestMatchers("/**/signup", "/**/signin") : This method specifies which HTTP requests should be matched by the provided patterns. 
        • .permitAll(): Allows unauthenticated access to the signup and signin endpoints. 필터를 통과하고나서도 authenticated되지 않은 request도 통과할 수 있도록 허락함.
        • * 는 wildcard이지만  a single path segment를 대신한다. 
        • ** 는 wildcard로써 뒤나 앞에 오는 모든 path segment을 대신한다. 단, **는 url내에서 한번만 쓰일 수 있다.
      • .anyRequest() : any HTTP request that does not match the patterns specified earlier  
        • .authenticated(): Requires authentication for all other requests.
  •  addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class): UsernamePasswordAuthenticationFilter는 스프링 security에서 기본적으로 지원하는 로그인기능으로 userdetail의 정보와 대조를 통해서 이루어진다. 해당 필터보다 Custom필터를 앞에두어 SecurityContext를 채우면 보안필터는 스킵할 수 있어서 인증방식을 지정하는 역하을 하는 코드라고 할 수 있다.

 

Url Base Authority Control vs Method Base Authority Control

  Url base의 경우 조금더 broad한 관점에서 제어를 할때 유리하다. 예를들어 특정 url주소는 user만 사용할 수 있다거나, 특정 url주소는 partner만 사용할 수 있다거나 할때 1차적으로 제어를 걸수 있다. 반면 Method base는 좀더 specific한 관점에서 제어할때 필수적이다. 특정 Id의 회원정보 수정에 접근할때 요청자의 Id가 특정 Id인 경우에만 접근가능하다든지 등의 접근 제어에 적합하다.

  컨트롤러 계층에서는 Interface들을 접하고, 조율하고 데이터를 전처리하는 계층이기때문에 Authority Control은 BusinessLogic에 가까우므로 Service Layer에 들어가는 것이 합당하다. 또한, ServiceLayer에 들어가야 어떤 경로든 간에 민감한 Logic을 제어할 수 있다.

 

Method Base

  • SecurityConfig annotation
    • @EnableMethodSecurity : SpringSecurityConfig에 부착. 메소드 베이스 Security Control을 적용시킴.
      • prePostEnabled (Default: false) : Enables the use of @PreAuthorize and @PostAuthorize annotations.
  • Method annotation
    • @PreAuthorize : to check a condition (usually based on the user's roles or permissions) before the method is executed.
    • @PostAuthorize : is used to check a condition after the method has executed. It can be used to enforce security constraints based on the method's return value. is less common but useful when you need to validate the result of a method before it’s returned to the caller.