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 Setprivileges; @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 extends GrantedAuthority> getAuthorities() { Listauthorities = 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 .