425. User Role and Authorities - dkkahm/study-springfamework5 GitHub Wiki

Roles and authorities

  • Role is considered a group of one or more authorities
  • Roles by default start with "ROLE_"
    • Configuration uses methods of hasRole() or hasAnyRole()
      • Must not prefix with ROLE_
  • Authorities are any string
    • Configuration use methods of hasAuthority() or hasAnyAuthority()
  • User assigned a role or a set of roles and a role has a set of defined authorities

Entities

  • Entity relationship
User <--(N)---(N)--> Role <--(N)---(N)--> Authority
  • User Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String username;
    private String password;

    @Singular
    @ManyToMany(cascade = { CascadeType.MERGE, CascadeType.PERSIST }, fetch = FetchType.EAGER)
    @JoinTable(name="user_role",
            joinColumns = {@JoinColumn(name="USER_ID", referencedColumnName = "ID")},
            inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "ID")})
    private Set<Role> roles;

    @Transient
    private Set<Authority> authorities;

    public Set<Authority> getAuthorities() {
        return this.roles.stream()
                .map(Role::getAuthorities)
                .flatMap(Set::stream)
                .collect(Collectors.toSet());
    }

    @Builder.Default
    private Boolean accountNonExpired = true;

    @Builder.Default
    private Boolean accountNonLocked = true;

    @Builder.Default
    private Boolean credentialsNonExpired = true;

    @Builder.Default
    private Boolean enabled = true;
}
  • Role Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    @ManyToMany(mappedBy = "roles")
    private Set<User> users;

    @Singular
    @ManyToMany(cascade = { CascadeType.MERGE, CascadeType.PERSIST}, fetch = FetchType.EAGER)
    @JoinTable(name="role_authority",
            joinColumns = {@JoinColumn(name="ROLE_ID", referencedColumnName = "ID")},
            inverseJoinColumns = {@JoinColumn(name = "AUTHORITY_ID", referencedColumnName = "ID")})
    private Set<Authority> authorities;
}
  • Authority Entity
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Authority {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String permission;

    @ManyToMany(mappedBy = "authorities")
    private Set<Role> roles;
}

Level of authentication

  • Anonymously - Not Authenticated
  • Remembered - Authenticated via Remember me cookie
  • Fully - Fully Authenticated

Security Expressions

  • permitAll
  • denyAll
  • isAnonymous
  • IsAuthenticated
    • Is Authenticated (Fully or Remembered)
  • isRememberMe
    • Is Authenticated with Remember Me Cookie
  • isFullyAuthenticated
  • hasRole
  • hasAnyRole
  • hasAuthority
  • hasAnyAuthorities
  • hasIpAddress

Method Security

  • Enable using @EnableGlobalMethodSecurity configuration annotation
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  • @Secured - accepts list of roles or IS_AUTHENTICATED_ANONYMOUSLY
  • @PreAuthorize - accepts security expression

To create UserDetails

    @Transactional
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        log.debug("Getting User info via JPA");

        User user = userRepository.findByUsername(s).orElseThrow(() -> new UsernameNotFoundException("User name: " + s + " not found."));

        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
                user.getEnabled(), user.getAccountNonExpired(), user.getCredentialsNonExpired(),
                user.getAccountNonLocked(), convertToSpringAuthorities(user.getAuthorities()));
    }

    private Collection<? extends GrantedAuthority> convertToSpringAuthorities(Set<Authority> authorities) {
        if(authorities != null && authorities.size() > 0) {
            return authorities.stream()
                    .map(Authority::getPermission)
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet());
        } else {
            return new HashSet<>();
        }
    }

Sample User Data

            Authority createBeer = authorityRepository.save(Authority.builder().permission("beer.create").build());
            Authority updateBeer = authorityRepository.save(Authority.builder().permission("beer.update").build());
            Authority readBeer = authorityRepository.save(Authority.builder().permission("beer.read").build());
            Authority deleteBeer = authorityRepository.save(Authority.builder().permission("beer.delete").build());

            Role adminRole = roleRepository.save(Role.builder().name("ADMIN").build());
            Role customerRole = roleRepository.save(Role.builder().name("CUSTOMER").build());
            Role userRole = roleRepository.save(Role.builder().name("USER").build());

            adminRole.setAuthorities(Set.of(createBeer, updateBeer, readBeer, deleteBeer));
            customerRole.setAuthorities(Set.of(readBeer));
            userRole.setAuthorities(Set.of(readBeer));
            roleRepository.saveAll(Arrays.asList(adminRole, customerRole, userRole));

            userRepository.save(User.builder()
                    .username("spring")
                    .password(encodePassword("guru"))
                    .role(adminRole)
                    .build());

            userRepository.save(User.builder()
                    .username("user")
                    .password(encodePassword("password"))
                    .role(userRole)
                    .build());

            userRepository.save(User.builder()
                    .username("scott")
                    .password(encodePassword("tiger"))
                    .role(customerRole)
                    .build());

Sample Application of Authority

    @PreAuthorize("hasAuthority('beer.read')")
    @GetMapping(produces = { "application/json" }, path = "beer")
    public ResponseEntity<BeerPagedList> listBeers(
    
    ....

    @PreAuthorize("hasAuthority('beer.create')")
    @PostMapping(path = "beer")
    public ResponseEntity saveNewBeer(

    ....

    @PreAuthorize("hasAuthority('beer.update')")
    @PutMapping(path = {"beer/{beerId}"}, produces = { "application/json" })
    public ResponseEntity updateBeer(

    ....

    @PreAuthorize("hasAuthority('beer.delete')")
    @DeleteMapping({"beer/{beerId}"})
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteBeer(

    ....

Permission Annotation

  • Define Annotation
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('beer.read')")
public @interface BeerReadPermission {
}
  • Apply Annotation
    @BeerReadPermission
    @GetMapping(produces = { "application/json" }, path = "beer")
    public ResponseEntity<BeerPagedList> listBeers(

Authority as Role (Simple)

  • User has authority as role ("ROLE_ADMIN" or "ROLE_USER", ...)
    private Collection<? extends GrantedAuthority> convertToSpringAuthorities(Set<Authority> authorities) {
        if(authorities != null && authorities.size() > 0) {
            return authorities.stream()
                    .map(Authority::getRole) // ROLE_ADMIN or ROLE_USER or ROLE_CUSTOMER
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toSet());
        } else {
            return new HashSet<>();
        }
    }
  • SecurityConfig
                    .antMatchers(HttpMethod.DELETE, "/api/v1/beer/**").hasRole("ADMIN")
                    .mvcMatchers(HttpMethod.GET, "/api/v1/beerUpc/{upc}").permitAll()
                    .antMatchers("/brewery/breweries")
                        .hasAnyRole("ADMIN", "CUSTOMER")
                    .antMatchers(HttpMethod.GET, "/brewery/api/v1/breweries")
                        .hasAnyRole("ADMIN", "CUSTOMER")
  • Method Security
    @Secured({"ROLE_ADMIN", "ROLE_CUSTOMER"})
    @GetMapping
    public String processFindFormReturnMany(Customer customer, BindingResult result, Model model){
    @PreAuthorize("hasRole('ADMIN')")
    @PostMapping("/new")
    public String processCreationForm(Customer customer) {
⚠️ **GitHub.com Fallback** ⚠️