SMALL

Spring Security를 처음 접하는 경우 권한 별 접근 제어는 대부분 

WebSecurityConfigurerAdapter를 상속받은 클래스 하위에 configure(HttpSecurity http) 메서드에 설정하게 된다. 

 

public class ServerSecurityConfiguration extends WebSecurityConfigurerAdapter {

	private final LoginService loginService;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
	    SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	    successHandler.setTargetUrlParameter("redirectTo");
	    successHandler.setDefaultTargetUrl(this.adminServer.path("/"));

	    http.authorizeRequests(
	        (authorizeRequests) -> authorizeRequests.antMatchers("/assets/**").permitAll() 
	            .antMatchers("/login").permitAll()
	            .antMatchers("/h2-console/**").permitAll()
	            .anyRequest().authenticated() 
	    ).httpBasic(Customizer.withDefaults()) 
	        .csrf((csrf) -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 
	            .ignoringRequestMatchers(
	                new AntPathRequestMatcher("/api/v1/**"),
	                new AntPathRequestMatcher("/h2-console/**")
	                
	            ))
	        .headers().frameOptions().disable().and()
	        .rememberMe((rememberMe) -> rememberMe.key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600));
	}
}

 

 

이 설정 이외에 만약 특정 메서드에서 권한 별 접근 제어를 하고 싶은 경우에는 어떠한 방법이 있는지 알아보도록 하자. 

 

Spring Security에서는 특정 Class, Method에 적용할 수 있는 Annotation을 제공한다.

해당 Annotation을 원하는 위치에 작성하면 해당 클래스 또는 메서드는 권한 체크하는 로직을 타게 된다. 

 

Spring Security에서는 다양한 Annotation을 제공하는데, 오늘은 간단하게 @Secure, @PreAuthorize를 알아보도록 하자.

 

@Secured

@Secured는 특정 권한만 접근이 가능하다는 것을 나타내는 Annotation이다. 

간단하게 특정 기능은 Admin만 이용하게 하고 싶은 경우 위의 Annotation을 붙여주면 된다. 

public class ProjectController {

    private final ProjectService projectService;

    @PostMapping("/api/v1/project")
    @Secured("ROLE_ADMIN")
    public String save(@RequestBody ProjectSaveRequestDto requestDto) throws DuplicateException {

        return projectService.save(requestDto);
    }
}

 

만약에 단순하게 특정 권한을 가진 사람이 아닌 다양한 조건이 들어가야 되는 경우에는 @Secured로 표현하기는 조금 힘들게 된다.

이러한 경우에는 @PreAuthorize, @PostAuthorize를 이용하면 좀 더 복잡한 케이스도 처리할 수 있게 된다.

해당 Annotation에서는 SpringEL을 사용할 수 있어서 다양한 권한에 따른 처리가 가능하다. 

 

Spring Expression Language에서는 다양한 Built-in expression을 제공한다. 

아래의 예시에서는 " isAutheticated()" 를 사용하였다. 

Spring EL에서 제공하는 Expression을 숙지하고 있으면 사용자가 직접 기능을 만들지 않고도 활용할 수 있어 유용하다. 

 

https://docs.spring.io/spring-security/reference/servlet/authorization/expression-based.html#el-common-built-in

 

Expression-Based Access Control :: Spring Security

Any Spring-EL functionality is available within the expression, so you can also access properties on the arguments. For example, if you wanted a particular method to only allow access to a user whose username matched that of the contact, you could write

docs.spring.io

 

@PreAuthorize("isAuthenticated() and (( #user.name == principal.name ) or hasRole('ROLE_ADMIN'))")
@RequestMapping( value = "", method = RequestMethod.PUT)
public ResponseEntity<Project> updateProject( User user ){
    updateProject.update( user );
    return new ResponseEntity<Project>( new Project(), HttpStatus.OK );
}

@PreAuthorize

추가적으로 @PreAuthorize 내에 #user.name를 사용한 부분을 보면

Annotation이 붙은 Method의 Param값을 위와 같이 활용하여 사용할 수 있게 된다. 

 

@PostAuthorize

@PostAuthorize의 경우에는 Method가 실행된 이후의 return값을 활용할 수 있다. 

아래와 같이 returnObject.name과 같이 "returnObject"를 사용하면 된다. 

@PostAuthorize("isAuthenticated() and (( returnObject.name == principal.name ) or hasRole('ROLE_ADMIN'))")
@RequestMapping( value = "/{id}", method = RequestMethod.GET )
public Project getProject( @PathVariable("id") long id ){
    return service.findOne(id);
}

 

Annotation 활성화 설정 추가

추가적으로, 이러한 Annotation을 활성화 시키기 위해서는 설정을 추가해 주어야 한다. 

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class ServerSecurityConfiguration extends WebSecurityConfigurerAdapter {

	private final LoginService loginService;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
	    
        //.....
   }
}
LIST
SMALL

Interceptor

 

Interceptor는 "가로채다"라는 의미를 가지고 있다.

Spring에서도 해당 의미와 유사하게 

Client에서 Server로 보낸 Request 객체를 Dispatcher Servlet의 Controller에 도달하기 전에 가로채는 역할을 한다.

 

Interceptor를 통해 가로챈 요청에 추가 로직을 넣을 수도 있고, Request 객체를 검증하는 작업도 할 수 있다. 

출처 : http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:ptl:dispatcherservlet

 

Interceptor를 사용했을 때의 장점은 아래와 같다

  • 공통 로직을 사용하는 경우 코드 누락에 대한 위험성 감수
  • 코드 재사용성 증가
    • 메모리 낭비, 서버 부하 감수 

 

어떠한 프로그램이든 공통 로직은 한 곳에서 관리하는 게 가장 좋다.

그런 면에서 Interceptor는 좋은 옵션을 제공한다. 

 

사용 예제

 

@Component
public class MyInterceptor implements HandlerInterceptor {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// do what you want .. 
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// do what you want .. 
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
 		   Object handler, Exception ex)
			throws Exception {
		// do what you want .. 
	}

}

 

Interceptor를 구현하기 위해서는 "HandlerInterceptor"를 Implements 받으면 된다.

해당 Interface에는 아래의 3가지 메서드를 제공한다.

  • preHandler(...)
    • Controller가 호출되기 전에 실행된다
    • return 값은 boolean으로 해당 값이 false이면 Controller가 호출되지 않는다
  • postHandler
    • Controller가 호출 된 이후지만, View가 생성되기 전에 실행된다
    • ModelAndView 객체를 인자로 받아 View를 생성하기 전에 해당 객체의 정보를 조작할 수 있다
  • afterCompletion
    • View의 모든 작업이 완료된 후에 호출된다

 

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
	
	private final MyInterceptor interceptor;
    
	@Override
	public void addInterceptors(InterceptorRegistry registry) {

		registry.addInterceptor(interceptor).addPathPatterns("/**");
	}
	
}

 

작성한 Interceptor를 Spring에 등록하기 위해서는 WebMvcConfigurer를 Implements 하여서 코드를 추가해야 한다. 

 

WebMvcConfigurer 해당 Interface에 addInterceptors라는 메서드에서 추가해 주면 된다. 

또한 해당 Interceptor가 어떠한 조건일 때 실행되고, 실행되지 않는 등의 설정을 추가할 수 있다.

 

 

 

 

LIST
SMALL

Spring Boot Appliction이 실행되는 시점에 특정 코드를 추가하고 싶은 경우가 존재할 것이다.

이런 경우에 어떻게 해야하는지 알아보도록 하자.

CommandLineRunner

 

@Component
public class InitCommand implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		// do somethings ..
	}
}

 

ApplicationRunner

 

@Component
public class InitAppliation implements ApplicationRunner {
	@Override
	public void run(ApplicationArguments args) throws Exception {
		// do somethings .. 
	}
}

 

차이점

두 interface 모두 run이라는 메서드를 제공하고, 해당 메서드에서 원하는 로직을 작성하면 된다.

그렇다면 이 둘의 차이점은 무엇일까? 

 

차이점으로는 메서드에 arguments이다. 

CommandLineRunner는 String Array를 Application Runner는 ApplicationArguments를 인자로 받는다.

그렇 각각의 값들을 어떻게 세팅하고 어떻게 꺼내 쓰는지 알아보도록 하자.

 

java -jar SpringApp.jar agrs1 args2 --name=junsu --type=child

 

위와 같은 형태로 Application을 실행시켰다고 하면 

CommandLineRunner에서는 String Array로 해당 값들을 아래와 같이 받을 수 있다.

	@Override
	public void run(String... args) throws Exception {
		System.out.println("#######################################");
		for (String str : args) {
			System.out.println(str);
		}
		
	}

 

ApplicationRunner에서는 ApplicationArguments의 메서드들을 이용해서 값들을 가져올 수 있다.

NonOptionArgs와 Option(Name, Value)로 나누어서 값을 가져올 수 있게 되어있다. 

 

	@Override
	public void run(ApplicationArguments args) throws Exception {

		System.out.println("*******************************************");
		args.getNonOptionArgs().forEach(str -> System.out.println(str));

		args.getOptionNames().forEach(str -> {
			System.out.print(str + " -- ");
			System.out.println(args.getOptionValues(str).get(0));
		});
		
		System.out.println(args.getOptionValues("name"));
	}

 

LIST
SMALL

Fetch Join은 기본 SQL 문법이 아니다.

JPQL( Java Persistence Query Language)에서 성능 최적화를 위해  제공하는 기능이다. 

연관 Entity or Collection을 SQL 한 번에 조회하기 위해 사용된다. 

JPQL ( Java Persistence Query Language ) : 객체 지향 쿼리 언어 
 - 테이블 대상으로 쿼리를 하는것이 아닌 객체를 대상으로 쿼리
 - SQL를 추상화한 것으로 특정 DB의 SQL에 의존하지 않는다

보통 아래와 같은 형태로 사용된다.

 

@Query("select m from Member m join fetch m.team")

 

실제 사용되는 예시를 보도록하자.

Member, Team Entity가 존재하고 Member는 하나의 Team에 속할 수 있고 Team은 여러 명의 Member를 가질 수 있다.

 

 

위와 같은 형태로 Member의 정보를 조회했을 때 해당 Member가 속한 Team 정보를 조회하고 싶은 경우에 사용될 수 있다.

 

@Getter
@NoArgsConstructor
@Entity
public class Member {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@ManyToOne
	private Team team;
	
	@Builder
	public Member(String name) {
		this.name = name;
	}
	
	public void updateTeam(Team team) {
		this.team = team;
	}
}
@Getter
@NoArgsConstructor
@Entity
public class Team {

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

	@Column
	private String name;
	
	@OneToMany(mappedBy = "team")
	private List<Member> members;
	
	@Builder
	public Team(String name) {
		this.name = name;
	}
}

 

기본적으로 JPA에서 제공하는 findAll 메서드를 호출하게 되면 Member List를 모두 조회해오는 Query와 각각의 Member에 매핑되는 Team을 조회하는 쿼리가 실행하게 된다. 

( 예를 들어 Member의 개수가 N 인 경우 Member List 조회 ( 1 ) + 각 Member에 매핑되는 Team 조회 ( N ) => N +1 )

 

memberRepository.findAll()

Hibernate:
    select member0_.id as id1_2_, member0_.name as name2_2_, member0_.team_id as team_id3_2_ from Member member0_
Hibernate:
    select team0_.id as id1_6_0_, team0_.name as name2_6_0_ from Team team0_ where team0_.id=?
Hibernate:
    select team0_.id as id1_6_0_, team0_.name as name2_6_0_ from Team team0_ where team0_.id=?

 

이러한 문제를 해결하기 위해서 JPQL에서 Fetch Join 기능을 제공한다.

 

public interface MemberRepository extends JpaRepository<Member, Long>{

	@Query("select m from Member m join fetch m.team")
	Set<Member> findAllJoinFetch();
}

 

memberRepository.findAllJoinFetch()

Hibernate: 
    select
        member0_.id as id1_2_0_,
        team1_.id as id1_6_1_,
        member0_.name as name2_2_0_,
        member0_.team_id as team_id3_2_0_,
        team1_.name as name2_6_1_ 
    from
        Member member0_ 
    inner join
        Team team1_ 
            on member0_.team_id=team1_.id

만약 실행되는 Query의 Join 형식도 정하고 싶은 경우에는 아래와 같이 지정하면 된다.

@Query("select m from Member m left join fetch m.team")

 

 

또한 Join을 한 대상에서 한 번 더 Join을 하고 싶은 경우에는 아래와 같이 작성하면 된다.

( Team에 연관 객체로 Coach가 있다고 가정 )

@Query("select m from Member m join fetch m.team t join fetch t.coach")

 

만약 이러한 형태가 아닌 기본 제공하는 findAll을 통해서 연관 객체를 조회하고 싶은 경우에는 FetchType을 EAGER로 지정하면 된다.

@Getter
@NoArgsConstructor
@Entity
public class Team {

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

	@Column
	private String name;
	
	@OneToMany(mappedBy = "team", fetch = FetchType.EAGER) // !!Added
	private List<Member> members;
	
	@Builder
	public Team(String name) {
		this.name = name;
	}
}

 

LIST

+ Recent posts