Рубрики
Без рубрики

Пользовательское выражение безопасности с помощью Spring Security

Руководство по созданию нового пользовательского выражения безопасности с помощью Spring Security, а затем по использованию нового выражения с аннотациями До и после авторизации.

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы сосредоточимся на создании пользовательского выражения безопасности с помощью Spring Security .

Иногда выражения, доступные в рамках, просто недостаточно выразительны. И в этих случаях относительно просто создать новое выражение, которое семантически богаче существующих.

Сначала мы обсудим , как создать пользовательский PermissionEvaluator , затем полностью настраиваемое выражение – и, наконец, как переопределить одно из встроенных выражений безопасности.

2. Пользовательская Сущность

Во-первых, давайте подготовим основу для создания новых выражений безопасности.

Давайте посмотрим на нашу Пользователя сущность, которая имеет Привилегии и Организацию :

@Entity
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

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

    private String password;

    @ManyToMany(fetch = FetchType.EAGER) 
    @JoinTable(name = "users_privileges", 
      joinColumns = 
        @JoinColumn(name = "user_id", referencedColumnName = "id"),
      inverseJoinColumns = 
        @JoinColumn(name = "privilege_id", referencedColumnName = "id")) 
    private Set privileges;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "organization_id", referencedColumnName = "id")
    private Organization organization;

    // standard getters and setters
}

И вот наша простая Привилегия :

@Entity
public class Privilege {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

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

    // standard getters and setters
}

И наша Организация :

@Entity
public class Organization {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

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

    // standard setters and getters
}

Наконец – мы будем использовать более простой пользовательский Принципал :

public class MyUserPrincipal implements UserDetails {

    private User user;

    public MyUserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public Collection getAuthorities() {
        List authorities = new ArrayList();
        for (Privilege privilege : user.getPrivileges()) {
            authorities.add(new SimpleGrantedAuthority(privilege.getName()));
        }
        return authorities;
    }
    
    ...
}

Когда все эти классы будут готовы, мы будем использовать наш пользовательский Принципал в базовой UserDetailsService реализации:

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new MyUserPrincipal(user);
    }
}

Как видите, в этих отношениях нет ничего сложного – у пользователя есть одна или несколько привилегий, и каждый пользователь принадлежит к одной организации.

3. Настройка данных

Далее – давайте инициализируем нашу базу данных с помощью простых тестовых данных:

@Component
public class SetupData {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PrivilegeRepository privilegeRepository;

    @Autowired
    private OrganizationRepository organizationRepository;

    @PostConstruct
    public void init() {
        initPrivileges();
        initOrganizations();
        initUsers();
    }
}

Вот наши init методы:

private void initPrivileges() {
    Privilege privilege1 = new Privilege("FOO_READ_PRIVILEGE");
    privilegeRepository.save(privilege1);

    Privilege privilege2 = new Privilege("FOO_WRITE_PRIVILEGE");
    privilegeRepository.save(privilege2);
}
private void initOrganizations() {
    Organization org1 = new Organization("FirstOrg");
    organizationRepository.save(org1);
    
    Organization org2 = new Organization("SecondOrg");
    organizationRepository.save(org2);
}
private void initUsers() {
    Privilege privilege1 = privilegeRepository.findByName("FOO_READ_PRIVILEGE");
    Privilege privilege2 = privilegeRepository.findByName("FOO_WRITE_PRIVILEGE");
    
    User user1 = new User();
    user1.setUsername("john");
    user1.setPassword("123");
    user1.setPrivileges(new HashSet(Arrays.asList(privilege1)));
    user1.setOrganization(organizationRepository.findByName("FirstOrg"));
    userRepository.save(user1);
    
    User user2 = new User();
    user2.setUsername("tom");
    user2.setPassword("111");
    user2.setPrivileges(new HashSet(Arrays.asList(privilege1, privilege2)));
    user2.setOrganization(organizationRepository.findByName("SecondOrg"));
    userRepository.save(user2);
}

Обратите внимание, что:

  • Пользователь “Джон” имеет только FOO_READ_PRIVILEGE
  • Пользователь “том” имеет как FOO_READ_PRIVILEGE , так и FOOD_WRITER_PRIVILEGE

4. Пользовательский Оценщик Разрешений

На этом этапе мы готовы приступить к реализации нашего нового выражения – с помощью нового, настраиваемого оценщика разрешений.

Мы собираемся использовать привилегии пользователя для защиты наших методов, но вместо использования жестко закодированных имен привилегий мы хотим достичь более открытой и гибкой реализации.

Давайте начнем.

4.1. Оценщик разрешений

Чтобы создать собственный пользовательский оценщик разрешений, нам необходимо реализовать интерфейс PermissionEvaluator :

public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(
      Authentication auth, Object targetDomainObject, Object permission) {
        if ((auth == null) || (targetDomainObject == null) || !(permission instanceof String)){
            return false;
        }
        String targetType = targetDomainObject.getClass().getSimpleName().toUpperCase();
        
        return hasPrivilege(auth, targetType, permission.toString().toUpperCase());
    }

    @Override
    public boolean hasPermission(
      Authentication auth, Serializable targetId, String targetType, Object permission) {
        if ((auth == null) || (targetType == null) || !(permission instanceof String)) {
            return false;
        }
        return hasPrivilege(auth, targetType.toUpperCase(), 
          permission.toString().toUpperCase());
    }
}

Вот наш hasPrivilege() метод:

private boolean hasPrivilege(Authentication auth, String targetType, String permission) {
    for (GrantedAuthority grantedAuth : auth.getAuthorities()) {
        if (grantedAuth.getAuthority().startsWith(targetType)) {
            if (grantedAuth.getAuthority().contains(permission)) {
                return true;
            }
        }
    }
    return false;
}

Теперь у нас есть новое выражение безопасности, доступное и готовое к использованию: hasPermission .

И поэтому вместо использования более жестко закодированной версии:

@PostAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")

Мы можем использовать использовать:

@PostAuthorize("hasPermission(returnObject, 'read')")

или

@PreAuthorize("hasPermission(#id, 'Foo', 'read')")

Примечание: #id относится к параметру метода, а ‘ Foo ‘ относится к типу целевого объекта.

4.2. Настройка безопасности метода

Недостаточно определить CustomPermissionEvaluator – нам также необходимо использовать его в конфигурации безопасности нашего метода:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = 
          new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return expressionHandler;
    }
}

4.3. Пример на практике

Давайте теперь начнем использовать новое выражение – в нескольких простых методах контроллера:

@Controller
public class MainController {
    
    @PostAuthorize("hasPermission(returnObject, 'read')")
    @GetMapping("/foos/{id}")
    @ResponseBody
    public Foo findById(@PathVariable long id) {
        return new Foo("Sample");
    }

    @PreAuthorize("hasPermission(#foo, 'write')")
    @PostMapping("/foos")
    @ResponseStatus(HttpStatus.CREATED)
    @ResponseBody
    public Foo create(@RequestBody Foo foo) {
        return foo;
    }
}

И вот мы идем – мы все настроены и используем новое выражение на практике.

4.4. Живой Тест

Давайте теперь напишем простые живые тесты – попадем в API и убедимся, что все в рабочем состоянии:

@Test
public void givenUserWithReadPrivilegeAndHasPermission_whenGetFooById_thenOK() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/foos/1");
    assertEquals(200, response.getStatusCode());
    assertTrue(response.asString().contains("id"));
}

@Test
public void givenUserWithNoWritePrivilegeAndHasPermission_whenPostFoo_thenForbidden() {
    Response response = givenAuth("john", "123").contentType(MediaType.APPLICATION_JSON_VALUE)
                                                .body(new Foo("sample"))
                                                .post("http://localhost:8082/foos");
    assertEquals(403, response.getStatusCode());
}

@Test
public void givenUserWithWritePrivilegeAndHasPermission_whenPostFoo_thenOk() {
    Response response = givenAuth("tom", "111").contentType(MediaType.APPLICATION_JSON_VALUE)
                                               .body(new Foo("sample"))
                                               .post("http://localhost:8082/foos");
    assertEquals(201, response.getStatusCode());
    assertTrue(response.asString().contains("id"));
}

А вот наш данный метод Auth() :

private RequestSpecification givenAuth(String username, String password) {
    FormAuthConfig formAuthConfig = 
      new FormAuthConfig("http://localhost:8082/login", "username", "password");
    
    return RestAssured.given().auth().form(username, password, formAuthConfig);
}

5. Новое Выражение Безопасности

С помощью предыдущего решения мы смогли определить и использовать выражение hasPermission , которое может быть весьма полезным.

Однако мы все еще несколько ограничены здесь именем и семантикой самого выражения.

Итак, в этом разделе мы собираемся полностью настроить – и мы собираемся реализовать выражение безопасности под названием isMember() – проверка того, является ли принципал членом Организации.

5.1. Выражение безопасности пользовательского метода

Чтобы создать это новое пользовательское выражение, нам нужно начать с реализации корневой заметки, в которой начинается оценка всех выражений безопасности:

public class CustomMethodSecurityExpressionRoot 
  extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {

    public CustomMethodSecurityExpressionRoot(Authentication authentication) {
        super(authentication);
    }

    public boolean isMember(Long OrganizationId) {
        User user = ((MyUserPrincipal) this.getPrincipal()).getUser();
        return user.getOrganization().getId().longValue() == OrganizationId.longValue();
    }

    ...
}

Теперь, как мы предоставили эту новую операцию прямо в корневой заметке здесь; is Member() используется для проверки, является ли текущий пользователь членом данной Организации .

Также обратите внимание, как мы расширили SecurityExpressionRoot , чтобы включить встроенные выражения.

5.2. Обработчик пользовательских выражений

Затем нам нужно ввести наш Пользовательский метод SecurityExpressionRoot в наш обработчик выражений:

public class CustomMethodSecurityExpressionHandler 
  extends DefaultMethodSecurityExpressionHandler {
    private AuthenticationTrustResolver trustResolver = 
      new AuthenticationTrustResolverImpl();

    @Override
    protected MethodSecurityExpressionOperations createSecurityExpressionRoot(
      Authentication authentication, MethodInvocation invocation) {
        CustomMethodSecurityExpressionRoot root = 
          new CustomMethodSecurityExpressionRoot(authentication);
        root.setPermissionEvaluator(getPermissionEvaluator());
        root.setTrustResolver(this.trustResolver);
        root.setRoleHierarchy(getRoleHierarchy());
        return root;
    }
}

5.3. Настройка безопасности метода

Теперь нам нужно использовать наш Пользовательский метод SecurityExpressionHandler в конфигурации безопасности метода:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        CustomMethodSecurityExpressionHandler expressionHandler = 
          new CustomMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
        return expressionHandler;
    }
}

5.4. Использование нового выражения

Вот простой пример защиты нашего метода контроллера с помощью isMember() :

@PreAuthorize("isMember(#id)")
@GetMapping("/organizations/{id}")
@ResponseBody
public Organization findOrgById(@PathVariable long id) {
    return organizationRepository.findOne(id);
}

5.5. Живой тест

Наконец, вот простой живой тест для пользователя ” john “:

@Test
public void givenUserMemberInOrganization_whenGetOrganization_thenOK() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/organizations/1");
    assertEquals(200, response.getStatusCode());
    assertTrue(response.asString().contains("id"));
}

@Test
public void givenUserMemberNotInOrganization_whenGetOrganization_thenForbidden() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/organizations/2");
    assertEquals(403, response.getStatusCode());
}

6. Отключите встроенное выражение безопасности

Наконец, давайте посмотрим, как переопределить встроенное выражение безопасности-мы обсудим отключение hasAuthority() .

6.1. Корень пользовательского выражения безопасности

Мы начнем аналогично, написав наш собственный SecurityExpressionRoot – главным образом потому, что встроенные методы являются окончательными , и поэтому мы не можем их переопределить:

public class MySecurityExpressionRoot implements MethodSecurityExpressionOperations {
    public MySecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        }
        this.authentication = authentication;
    }

    @Override
    public final boolean hasAuthority(String authority) {
        throw new RuntimeException("method hasAuthority() not allowed");
    }
    ...
}

После определения этой корневой заметки нам придется ввести ее в обработчик выражений, а затем подключить этот обработчик к нашей конфигурации – точно так же, как мы сделали это выше в разделе 5.

6.2. Пример – Использование выражения

Теперь, если мы хотим использовать hasAuthority() для защиты методов – как показано ниже, он вызовет RuntimeException при попытке доступа к методу:

@PreAuthorize("hasAuthority('FOO_READ_PRIVILEGE')")
@GetMapping("/foos")
@ResponseBody
public Foo findFooByName(@RequestParam String name) {
    return new Foo(name);
}

6.3. Живой тест

Наконец, вот наш простой тест:

@Test
public void givenDisabledSecurityExpression_whenGetFooByName_thenError() {
    Response response = givenAuth("john", "123").get("http://localhost:8082/foos?name=sample");
    assertEquals(500, response.getStatusCode());
    assertTrue(response.asString().contains("method hasAuthority() not allowed"));
}

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

В этом руководстве мы глубоко погрузились в различные способы реализации пользовательского выражения безопасности в Spring Security, если существующих недостаточно.

И, как всегда, полный исходный код можно найти на GitHub .