Перейти к основному содержимому

Spring Security против Apache Широ

· 8 мин. чтения

1. Обзор

Безопасность является первоочередной задачей в мире разработки приложений, особенно в области корпоративных веб-приложений и мобильных приложений.

В этом кратком руководстве мы сравним два популярных фреймворка Java Security — Apache Shiro и Spring Security .

2. Немного предыстории

Apache Shiro родился в 2004 году как JSecurity и был принят Apache Foundation в 2008 году. На сегодняшний день он видел много выпусков, последним на момент написания этого является 1.5.3.

Spring Security начинался как Acegi в 2003 году и был включен в Spring Framework с его первым общедоступным выпуском в 2008 году. С момента своего создания он прошел несколько итераций, и на момент написания текущей общедоступной версии является 5.3.2.

Обе технологии предлагают поддержку аутентификации и авторизации, а также решения для криптографии и управления сеансами . Кроме того, Spring Security обеспечивает первоклассную защиту от таких атак, как CSRF и фиксация сеанса.

В следующих нескольких разделах мы увидим примеры того, как две технологии обрабатывают аутентификацию и авторизацию. Для простоты мы будем использовать базовые приложения MVC на базе Spring Boot с шаблонами FreeMarker .

3. Настройка Apache Широ

Для начала давайте посмотрим, чем отличаются конфигурации двух фреймворков.

3.1. Зависимости Maven

Поскольку мы будем использовать Shiro в приложении Spring Boot, нам понадобится его стартер и модуль 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. Создание области

Чтобы объявить пользователей с их ролями и разрешениями в памяти, нам нужно создать область, расширяющую JdbcRealm Широ . Мы определим двух пользователей — Тома и Джерри с ролями USER и ADMIN соответственно:

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-компонент в наше загрузочное приложение:

@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 может быть доступна только аутентифицированным пользователям.

Это все, что нам нужно для настройки, все остальное за нас сделает Широ.

4. Настройка безопасности Spring

Теперь давайте посмотрим, как добиться того же в Spring.

4.1. Зависимости Maven

Во-первых, зависимости:

<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. Класс конфигурации

Далее мы определим нашу конфигурацию Spring Security в классе SecurityConfig , расширяющем WebSecurityConfigurerAdapter :

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/index", "/login").permitAll()
.antMatchers("/home", "/logout").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN"))
.formLogin(formLogin -> formLogin
.loginPage("/login")
.failureUrl("/login-error"));
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("Jerry")
.password(passwordEncoder().encode("password"))
.authorities("READ", "WRITE")
.roles("ADMIN")
.and()
.withUser("Tom")
.password(passwordEncoder().encode("password"))
.authorities("READ")
.roles("USER");
}

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

Как мы видим, мы создали объект AuthenticationManagerBuilder для объявления наших пользователей с их ролями и полномочиями. Кроме того, мы закодировали пароли с помощью BCryptPasswordEncoder .

Spring Security также предоставляет нам свой объект HttpSecurity для дальнейших конфигураций. В нашем примере мы разрешили:

  • всем доступ к нашему индексу и страницам входа
  • только аутентифицированные пользователи могут заходить на домашнюю страницу и выходить из системы
  • только пользователи с ролью ADMIN могут получить доступ к страницам администратора

Мы также определили поддержку проверки подлинности на основе форм для отправки пользователей в конечную точку входа . В случае сбоя входа наши пользователи будут перенаправлены на /login-error .

5. Контроллеры и конечные точки

Теперь давайте посмотрим на наши сопоставления веб-контроллера для двух приложений. Хотя они будут использовать одни и те же конечные точки, некоторые реализации будут отличаться.

5.1. Конечные точки для рендеринга просмотра

Для конечных точек, отображающих представление, реализации одинаковы:

@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, возвращают index.ftl на корневую конечную точку, login.ftl на конечную точку входа и home.ftl на домашнюю конечную точку.

Однако определение метода addUserAttributes в конечной точке /home будет различаться между двумя контроллерами. Этот метод анализирует атрибуты текущего пользователя, вошедшего в систему.

Широ предоставляет 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 предоставляет объект Authentication из своего контекста SecurityContextHolder для этой цели:

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

В Широ мы сопоставляем учетные данные, которые вводит пользователь, с 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 это всего лишь вопрос перенаправления на домашнюю страницу. Процесс входа в систему Spring, обрабатываемый его UsernamePasswordAuthenticationFilter , прозрачен для нас :

@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
return "redirect:/home";
}

5.3. Конечная точка только для администратора

Теперь давайте рассмотрим сценарий, в котором нам нужно выполнить доступ на основе ролей. Допустим, у нас есть конечная точка /admin , доступ к которой должен быть разрешен только для роли ADMIN.

Давайте посмотрим, как это сделать в Широ:

@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. Выход из системы

Наконец, давайте реализуем конечную точку выхода из системы.

В Широ мы просто вызовем Subject#logout :

@PostMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/";
}

Для Spring мы не определили какое-либо сопоставление для выхода из системы. В этом случае срабатывает механизм выхода из системы по умолчанию, который применяется автоматически, поскольку мы расширили WebSecurityConfigurerAdapter в нашей конфигурации.

6. Apache Широ против Spring Security

Теперь, когда мы рассмотрели различия в реализации, давайте рассмотрим несколько других аспектов.

Что касается поддержки сообщества, Spring Framework в целом имеет огромное сообщество разработчиков , активно участвующих в его разработке и использовании. Поскольку Spring Security является частью зонтика, он должен пользоваться теми же преимуществами. Широ хоть и популярен, но не имеет такой огромной поддержки.

Что касается документации, Spring снова является победителем.

Однако есть некоторая кривая обучения, связанная с Spring Security. Широ, с другой стороны, легко понять . Для настольных приложений настройка через shiro.ini еще проще.

Но опять же, как мы видели в наших примерах фрагментов, Spring Security отлично справляется с разделением бизнес-логики и безопасности и действительно предлагает безопасность как сквозную проблему.

7. Заключение

В этом туториале мы сравнили Apache Shiro со Spring Security .

Мы только что коснулись того, что могут предложить эти фреймворки, и нам предстоит еще многое изучить. Существует довольно много альтернатив, таких как JAAS и OACC . Тем не менее, с его преимуществами, Spring Security , кажется, выигрывает на данный момент.

Как всегда, исходный код доступен на GitHub .