1. 개요
Security은 애플리케이션 개발 세계, 특히 엔터프라이즈 웹 및 모바일 애플리케이션 영역에서 주요 관심사입니다.
이 빠른 사용방법(예제)에서는 두 가지 인기 있는 Java Security 프레임워크인 Apache Shiro 및 Spring Security를 비교합니다 .
2. 약간의 배경
Apache Shiro는 2004년에 JSecurity로 탄생했으며 2008년에 Apache Foundation에 의해 승인되었습니다. 현재까지 많은 릴리스가 있었으며 이 문서를 작성하는 시점의 최신 버전은 1.5.3입니다.
Spring Security는 2003년에 Acegi로 시작하여 2008년 첫 공개 릴리스와 함께 Spring Framework에 통합되었습니다. 처음부터 여러 번의 반복을 거쳤으며 현재 GA 버전은 5.3.2입니다.
두 기술 모두 암호화 및 세션 관리 솔루션과 함께 인증 및 권한 부여 지원을 제공합니다 . 또한 Spring Security는 CSRF 및 세션 고정과 같은 공격에 대해 최고 수준의 보호 기능을 제공합니다.
다음 몇 섹션에서는 두 기술이 인증 및 권한 부여를 처리하는 방법에 대한 예를 살펴보겠습니다. 간단하게 유지하기 위해 FreeMarker 템플릿 과 함께 기본 Spring Boot 기반 MVC 애플리케이션을 사용할 것입니다 .
3. 아파치 시로 구성
먼저 두 프레임워크 간의 구성이 어떻게 다른지 살펴보겠습니다.
3.1. 메이븐 의존성
Spring Boot 앱에서 Shiro를 사용할 것이므로 스타터와 shiro-core 모듈이 필요합니다.
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
최신 버전은 Maven Central 에서 찾을 수 있습니다 .
3.2. 영역 만들기
역할과 권한이 있는 사용자를 메모리 내에서 선언하려면 Shiro의 JdbcRealm을 확장하는 영역을 만들어야 합니다 . 각각 USER 및 ADMIN 역할을 가진 두 명의 사용자(Tom 및 Jerry)를 정의합니다.
public class CustomRealm extends JdbcRealm {
private Map<String, String> credentials = new HashMap<>();
private Map<String, Set> roles = new HashMap<>();
private Map<String, Set> permissions = new HashMap<>();
{
credentials.put("Tom", "password");
credentials.put("Jerry", "password");
roles.put("Jerry", new HashSet<>(Arrays.asList("ADMIN")));
roles.put("Tom", new HashSet<>(Arrays.asList("USER")));
permissions.put("ADMIN", new HashSet<>(Arrays.asList("READ", "WRITE")));
permissions.put("USER", new HashSet<>(Arrays.asList("READ")));
}
}
다음으로 이 인증 및 권한 부여 검색을 활성화하려면 몇 가지 방법을 재정의해야 합니다.
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if (userToken.getUsername() == null || userToken.getUsername().isEmpty() ||
!credentials.containsKey(userToken.getUsername())) {
throw new UnknownAccountException("User doesn't exist");
}
return new SimpleAuthenticationInfo(userToken.getUsername(),
credentials.get(userToken.getUsername()), getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Set roles = new HashSet<>();
Set permissions = new HashSet<>();
for (Object user : principals) {
try {
roles.addAll(getRoleNamesForUser(null, (String) user));
permissions.addAll(getPermissions(null, null, roles));
} catch (SQLException e) {
logger.error(e.getMessage());
}
}
SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(roles);
authInfo.setStringPermissions(permissions);
return authInfo;
}
doGetAuthorizationInfo 메서드는 두 가지 도우미 메서드를 사용하여 사용자의 역할과 권한을 가져옵니다.
@Override
protected Set getRoleNamesForUser(Connection conn, String username)
throws SQLException {
if (!roles.containsKey(username)) {
throw new SQLException("User doesn't exist");
}
return roles.get(username);
}
@Override
protected Set getPermissions(Connection conn, String username, Collection roles)
throws SQLException {
Set userPermissions = new HashSet<>();
for (String role : roles) {
if (!permissions.containsKey(role)) {
throw new SQLException("Role doesn't exist");
}
userPermissions.addAll(permissions.get(role));
}
return userPermissions;
}
다음으로 이 CustomRealm을 부트 애플리케이션의 빈으로 포함해야 합니다 .
@Bean
public Realm customRealm() {
return new CustomRealm();
}
또한 엔드포인트에 대한 인증을 구성하려면 다른 bean이 필요합니다.
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition();
filter.addPathDefinition("/home", "authc");
filter.addPathDefinition("/**", "anon");
return filter;
}
여기에서 DefaultShiroFilterChainDefinition 인스턴스를 사용하여 인증된 사용자만 /home 엔드포인트에 액세스할 수 있도록 지정했습니다 .
이것이 구성에 필요한 전부이며 Shiro가 나머지 작업을 수행합니다.
4. 스프링 Security 설정
이제 Spring에서 동일한 작업을 수행하는 방법을 살펴보겠습니다.
4.1. 메이븐 의존성
먼저 의존성:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
최신 버전은 Maven Central 에서 찾을 수 있습니다 .
4.2. 구성 클래스
다음으로 SecurityConfig 클래스에서 Spring Security 구성을 정의합니다 .
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests(authorize -> authorize.antMatchers("/index", "/login")
.permitAll()
.antMatchers("/home", "/logout")
.authenticated()
.antMatchers("/admin/**")
.hasRole("ADMIN"))
.formLogin(formLogin -> formLogin.loginPage("/login")
.failureUrl("/login-error"));
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() throws Exception {
UserDetails jerry = User.withUsername("Jerry")
.password(passwordEncoder().encode("password"))
.authorities("READ", "WRITE")
.roles("ADMIN")
.build();
UserDetails tom = User.withUsername("Tom")
.password(passwordEncoder().encode("password"))
.authorities("READ")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(jerry, tom);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
보시다시피 UserDetails 개체를 만들어 사용자의 역할과 권한을 선언했습니다. 또한 BCryptPasswordEncoder 를 사용하여 암호를 인코딩했습니다 .
Spring Security는 또한 추가 구성을 위해 HttpSecurity 객체를 제공합니다 . 이 예에서는 다음을 허용했습니다.
- 누구나 색인 및 로그인 페이지 에 액세스할 수 있습니다.
- 인증된 사용자만 홈페이지 진입 및 로그아웃
- 관리자 페이지에 액세스할 수 있는 ADMIN 역할을 가진 사용자만
또한 사용자를 로그인 Endpoints 으로 보내는 양식 기반 인증에 대한 지원을 정의했습니다 . 로그인에 실패하면 사용자는 /login-error 로 리디렉션됩니다 .
5. 컨트롤러 및 엔드포인트
이제 두 애플리케이션에 대한 웹 컨트롤러 매핑을 살펴보겠습니다. 동일한 Endpoints을 사용하지만 일부 구현은 다를 수 있습니다.
5.1. 뷰 렌더링을 위한 Endpoints
보기를 렌더링하는 Endpoints의 경우 구현은 동일합니다.
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/login")
public String showLoginPage() {
return "login";
}
@GetMapping("/home")
public String getMeHome(Model model) {
addUserAttributes(model);
return "home";
}
컨트롤러 구현인 Shiro와 Spring Security 모두 루트 Endpoints에서 index.ftl , 로그인 Endpoints에서 login.ftl , 홈 Endpoints에서 home.ftl을 반환합니다.
그러나 /home 엔드포인트 에 있는 addUserAttributes 메서드의 정의는 두 컨트롤러 간에 다릅니다. 이 메소드는 현재 로그인한 사용자의 속성을 검사합니다.
Shiro는 SecurityUtils#getSubject를 제공하여 현재 Subject 와 해당 역할 및 권한을 검색합니다.
private void addUserAttributes(Model model) {
Subject currentUser = SecurityUtils.getSubject();
String permission = "";
if (currentUser.hasRole("ADMIN")) {
model.addAttribute("role", "ADMIN");
} else if (currentUser.hasRole("USER")) {
model.addAttribute("role", "USER");
}
if (currentUser.isPermitted("READ")) {
permission = permission + " READ";
}
if (currentUser.isPermitted("WRITE")) {
permission = permission + " WRITE";
}
model.addAttribute("username", currentUser.getPrincipal());
model.addAttribute("permission", permission);
}
반면 Spring Security는 이러한 목적을 위해 SecurityContextHolder 의 컨텍스트 에서 Authentication 객체를 제공합니다.
private void addUserAttributes(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && !auth.getClass().equals(AnonymousAuthenticationToken.class)) {
User user = (User) auth.getPrincipal();
model.addAttribute("username", user.getUsername());
Collection<GrantedAuthority> authorities = user.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().contains("USER")) {
model.addAttribute("role", "USER");
model.addAttribute("permissions", "READ");
} else if (authority.getAuthority().contains("ADMIN")) {
model.addAttribute("role", "ADMIN");
model.addAttribute("permissions", "READ WRITE");
}
}
}
}
5.2. POST 로그인 Endpoints
Shiro에서는 사용자가 POJO에 입력하는 자격 증명을 매핑합니다.
public class UserCredentials {
private String username;
private String password;
// getters and setters
}
그런 다음 UsernamePasswordToken을 생성하여 사용자 또는 Subject를 로그인 합니다.
@PostMapping("/login")
public String doLogin(HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) {
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(credentials.getUsername(),
credentials.getPassword());
try {
subject.login(token);
} catch (AuthenticationException ae) {
logger.error(ae.getMessage());
attr.addFlashAttribute("error", "Invalid Credentials");
return "redirect:/login";
}
}
return "redirect:/home";
}
Spring Security 측에서 이것은 홈 페이지로의 리디렉션 문제일 뿐입니다. UsernamePasswordAuthenticationFilter 에 의해 처리되는 Spring의 로그인 프로세스는 우리에게 투명합니다 .
@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
return "redirect:/home";
}
5.3. 관리자 전용 Endpoints
이제 역할 기반 액세스를 수행해야 하는 시나리오를 살펴보겠습니다. ADMIN 역할에만 액세스가 허용되어야 하는 /admin 엔드포인트 가 있다고 가정해 보겠습니다 .
Shiro에서 이 작업을 수행하는 방법을 살펴보겠습니다.
@GetMapping("/admin")
public String adminOnly(ModelMap modelMap) {
addUserAttributes(modelMap);
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("ADMIN")) {
modelMap.addAttribute("adminContent", "only admin can view this");
}
return "home";
}
여기에서 현재 로그인한 사용자를 추출하고 ADMIN 역할이 있는지 확인하고 그에 따라 콘텐츠를 추가했습니다.
Spring Security에서는 프로그래밍 방식으로 역할을 확인할 필요가 없으며 SecurityConfig에서 누가 이 엔드포인트에 도달할 수 있는지 이미 정의 했습니다 . 이제 비즈니스 로직을 추가하기만 하면 됩니다.
@GetMapping("/admin")
public String adminOnly(HttpServletRequest req, Model model) {
addUserAttributes(model);
model.addAttribute("adminContent", "only admin can view this");
return "home";
}
5.4. 로그아웃 Endpoints
마지막으로 로그아웃 Endpoints을 구현해 보겠습니다.
Shiro에서는 단순히 Subject#logout을 호출합니다 .
@PostMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/";
}
Spring의 경우 로그아웃에 대한 매핑을 정의하지 않았습니다. 이 경우 구성에서 SecurityFilterChain 빈을 생성한 이후 자동으로 적용되는 기본 로그아웃 메커니즘이 시작됩니다.
6. 아파치 시로 VS 스프링 시큐리티
이제 구현 차이점을 살펴보았으므로 몇 가지 다른 측면을 살펴보겠습니다.
커뮤니티 지원 측면에서 Spring Framework에는 일반적으로 개발 및 사용에 적극적으로 참여하는 거대한 개발자 커뮤니티가 있습니다 . Spring Security는 우산의 일부이므로 동일한 이점을 누릴 수 있어야 합니다. Shiro는 인기가 있지만 그렇게 엄청난 지원을 받지 못합니다.
문서와 관련하여 Spring이 다시 승자입니다.
그러나 Spring Security와 관련된 약간의 학습 곡선이 있습니다. 반면 Shiro는 이해하기 쉽습니다 . 데스크톱 애플리케이션의 경우 shiro.ini를 통한 구성이 훨씬 더 쉽습니다.
그러나 예제 스니펫에서 본 것처럼 Spring Security는 비즈니스 로직과 Security을 분리 하는 훌륭한 작업을 수행 하고 교차 절단 문제로 Security을 제공합니다.
7. 결론
이 예제에서는 Apache Shiro와 Spring Security를 비교했습니다 .
우리는 이러한 프레임워크가 제공해야 하는 것의 표면을 스쳐 지나갔고 더 탐구할 것이 많습니다. JAAS 및 OACC 와 같은 몇 가지 대안이 있습니다 . 여전히 장점이 있는 Spring Security가 이 시점에서 승리하는 것 같습니다.
항상 그렇듯이 소스 코드는 GitHub에서 사용할 수 있습니다 .