- 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
User <--(N)---(N)--> Role <--(N)---(N)--> Authority
@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;
}
@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;
}
@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;
}
- Anonymously - Not Authenticated
- Remembered - Authenticated via Remember me cookie
- Fully - Fully Authenticated
- permitAll
- denyAll
- isAnonymous
- IsAuthenticated
- Is Authenticated (Fully or Remembered)
- isRememberMe
- Is Authenticated with Remember Me Cookie
- isFullyAuthenticated
- hasRole
- hasAnyRole
- hasAuthority
- hasAnyAuthorities
- hasIpAddress
- 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
@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<>();
}
}
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(
....
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('beer.read')")
public @interface BeerReadPermission {
}
@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<>();
}
}
.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")
@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) {