With this example, you will understand the spring security mechanism. No need to database. Just we are adding some users and authorities (roles). In this example we used Basic Authentication (username, password)
@Bean
public UserDetailsService users() {
UserDetails user =
User.builder()
.username("user")
.password("pass")
.passwordEncoder(passwordEncoder::encode)
// .password(passwordEncoder.encode("pass"))
.roles("USER")
.build();
...
return new InMemoryUserDetailsManager(user, admin, mod);
}
- to allow endpoints we can use:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) ->
web.ignoring()
.requestMatchers(
new AntPathRequestMatcher("/auth/**"),
new AntPathRequestMatcher("/public/**"));
}
- also we can configure http security options:
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.headers(x -> x.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.csrf(AbstractHttpConfigurer::disable)
.cors(Customizer.withDefaults())
// authenticate any request except web.ignoring()
// also you can allow some endpoints here:
// x.requestMatchers(new AntPathRequestMatcher("/auth/**")).permitAll()
.authorizeHttpRequests(x -> x.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return httpSecurity.build();
}
With this example, you will able to add hardcoded roles to user. Many projects we don't need to add dynamic roles and store them via different table in database.
NOTE: After first example, if we want to retrieve users from database we have to implement our custom user details service, and use spring-security's UserDetails class instead our User class:
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
var user = userService.getByUsername(username);
if (user.isEmpty()) {
throw new EntityNotFoundException();
}
return new CustomUserDetails(user.get());
}
}
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.user.getRoles();
}
@Override
public String getPassword() {
return this.user.getPassword();
}
@Override
public String getUsername() {
return this.user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.user.isEnabled();
}
}
.....
public class User extends BaseEntity {
.....
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
@JoinTable(
name = "user_role",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private Set<Role> roles;
}
....
public class Role extends BaseEntity implements GrantedAuthority {
@Column(name = "role_name")
private String name;
@ManyToMany(
fetch = FetchType.LAZY,
cascade = {CascadeType.PERSIST, CascadeType.MERGE},
mappedBy = "roles")
@JsonIgnore
private Set<User> users = new HashSet<>();
@Override
public String getAuthority() {
return getName();
}
}
With this example, you will able to allow or block access to endpoints by Roles. We are using AuthorizationManager (AccessDecisionVoter is deprecated) to decide.
@Component
@RequiredArgsConstructor
public class RoleBasedVoter implements AuthorizationManager<RequestAuthorizationContext> {
private final RoleRepository roleRepository;
@Override
public AuthorizationDecision check(
Supplier<Authentication> authentication, RequestAuthorizationContext object) {
if (authentication.get().getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.get().getPrincipal();
String requestUrl = object.getRequest().getRequestURI();
List<Role> roles = roleRepository.findByUsers_Username(userDetails.getUsername());
for (Role role : roles) {
if (role.getRestrictedEndpoints().contains(requestUrl)) {
return new AuthorizationDecision(false);
}
}
}
return new AuthorizationDecision(true);
}
}
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final RoleBasedVoter roleBasedVoter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
....
.userDetailsService(userDetailsService)
.authorizeHttpRequests(x -> x.anyRequest().access(roleBasedVoter))
....
- JWT is a token based authentication mechanism. Once you got token, you can use it until expire. Also we use refresh token to get new token after your access token get expired
https://www.youtube.com/watch?v=BoioooM1vL8