BAEL-5395 - Authentication with Spring Security and MongoDB (#11883)

* auth with spring security and mongodb

* fix PMD

* fix missing application property in tests

* fix: code style

* fix: static finale case

* fix: omit Javadoc
This commit is contained in:
lucaCambi77 2022-03-11 03:48:37 +01:00 committed by GitHub
parent c6bd572eda
commit bfd778c028
14 changed files with 485 additions and 1 deletions

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-web-boot-3</artifactId>
<version>0.0.1-SNAPSHOT</version>
@ -23,6 +24,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

View File

@ -0,0 +1,18 @@
package com.baeldung.mongoauth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import com.baeldung.mongoauth.config.MongoConfig;
import com.baeldung.mongoauth.config.SecurityConfig;
@SpringBootApplication
@Import({ SecurityConfig.class, MongoConfig.class })
public class MongoAuthApplication {
public static void main(String... args) {
SpringApplication.run(MongoAuthApplication.class, args);
}
}

View File

@ -0,0 +1,40 @@
package com.baeldung.mongoauth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.util.SocketUtils;
import com.mongodb.client.MongoClients;
import de.flapdoodle.embed.mongo.MongodExecutable;
import de.flapdoodle.embed.mongo.MongodStarter;
import de.flapdoodle.embed.mongo.config.ImmutableMongodConfig;
import de.flapdoodle.embed.mongo.config.MongodConfig;
import de.flapdoodle.embed.mongo.config.Net;
import de.flapdoodle.embed.mongo.distribution.Version;
import de.flapdoodle.embed.process.runtime.Network;
@Configuration
public class MongoConfig {
private static final String CONNECTION_STRING = "mongodb://%s:%d";
private static final String HOST = "localhost";
@Bean
public MongoTemplate mongoTemplate() throws Exception {
int randomPort = SocketUtils.findAvailableTcpPort();
ImmutableMongodConfig mongoDbConfig = MongodConfig.builder()
.version(Version.Main.PRODUCTION)
.net(new Net(HOST, randomPort, Network.localhostIsIPv6()))
.build();
MongodStarter starter = MongodStarter.getDefaultInstance();
MongodExecutable mongodExecutable = starter.prepare(mongoDbConfig);
mongodExecutable.start();
return new MongoTemplate(MongoClients.create(String.format(CONNECTION_STRING, HOST, randomPort)), "mongo_auth");
}
}

View File

@ -0,0 +1,59 @@
package com.baeldung.mongoauth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public AuthenticationManager customAuthenticationManager() throws Exception {
return authenticationManager();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(@Autowired AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.and()
.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}

View File

@ -0,0 +1,23 @@
package com.baeldung.mongoauth.controller;
import javax.annotation.security.RolesAllowed;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ResourceController {
@RolesAllowed("ROLE_ADMIN")
@GetMapping("/admin")
public String admin() {
return "Hello Admin!";
}
@RolesAllowed({ "ROLE_ADMIN", "ROLE_USER" })
@GetMapping("/user")
public String user() {
return "Hello User!";
}
}

View File

@ -0,0 +1,13 @@
package com.baeldung.mongoauth.domain;
public class Role {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -0,0 +1,83 @@
package com.baeldung.mongoauth.domain;
import java.util.Objects;
import java.util.Set;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoId;
import org.springframework.security.core.userdetails.UserDetails;
@Document
public class User implements UserDetails {
private @MongoId ObjectId id;
private String username;
private String password;
private Set<UserRole> userRoles;
public ObjectId getId() {
return id;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setUserRoles(Set<UserRole> userRoles) {
this.userRoles = userRoles;
}
@Override
public Set<UserRole> getAuthorities() {
return this.userRoles;
}
@Override
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
User user = (User) o;
return Objects.equals(username, user.username);
}
@Override
public int hashCode() {
return Objects.hash(username);
}
}

View File

@ -0,0 +1,21 @@
package com.baeldung.mongoauth.domain;
import org.springframework.security.core.GrantedAuthority;
public class UserRole implements GrantedAuthority {
private Role role;
@Override
public String getAuthority() {
return role.getName();
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
}

View File

@ -0,0 +1,13 @@
package com.baeldung.mongoauth.repository;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import com.baeldung.mongoauth.domain.User;
public interface UserRepository extends MongoRepository<User, String> {
@Query("{username:'?0'}")
User findUserByUsername(String username);
}

View File

@ -0,0 +1,41 @@
package com.baeldung.mongoauth.service;
import java.util.HashSet;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.baeldung.mongoauth.repository.UserRepository;
@Service
public class MongoAuthUserDetailService implements UserDetailsService {
private final UserRepository userRepository;
public MongoAuthUserDetailService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
com.baeldung.mongoauth.domain.User user = userRepository.findUserByUsername(userName);
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
user.getAuthorities()
.forEach(role -> {
grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole()
.getName()));
});
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}

View File

@ -0,0 +1,5 @@
package com.baeldung.mongoauth.service;
public interface SecurityService {
boolean login(String username, String password);
}

View File

@ -0,0 +1,39 @@
package com.baeldung.mongoauth.service;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
@Service
public class SecurityServiceImpl implements SecurityService {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
public SecurityServiceImpl(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
}
@Override
public boolean login(String username, String password) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
authenticationManager.authenticate(usernamePasswordAuthenticationToken);
if (usernamePasswordAuthenticationToken.isAuthenticated()) {
SecurityContextHolder.getContext()
.setAuthentication(usernamePasswordAuthenticationToken);
return true;
}
return false;
}
}

View File

@ -0,0 +1 @@
spring.mongodb.embedded.version=4.4.9

View File

@ -0,0 +1,118 @@
package com.baeldung.mongoauth;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections;
import java.util.HashSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.baeldung.mongoauth.domain.Role;
import com.baeldung.mongoauth.domain.User;
import com.baeldung.mongoauth.domain.UserRole;
@SpringBootTest(classes = { MongoAuthApplication.class })
@AutoConfigureMockMvc
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class MongoAuthApplicationIntegrationTest {
@Autowired
private WebApplicationContext context;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
private MockMvc mvc;
private static final String USER_NAME = "user@gmail.com";
private static final String ADMIN_NAME = "admin@gmail.com";
private static final String PASSWORD = "password";
@BeforeEach
public void setup() {
setUp();
mvc = MockMvcBuilders.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
private void setUp() {
Role roleUser = new Role();
roleUser.setName("ROLE_USER");
mongoTemplate.save(roleUser);
User user = new User();
user.setUsername(USER_NAME);
user.setPassword(bCryptPasswordEncoder.encode(PASSWORD));
UserRole userRole = new UserRole();
userRole.setRole(roleUser);
user.setUserRoles(new HashSet<>(Collections.singletonList(userRole)));
mongoTemplate.save(user);
User admin = new User();
admin.setUsername(ADMIN_NAME);
admin.setPassword(bCryptPasswordEncoder.encode(PASSWORD));
Role roleAdmin = new Role();
roleAdmin.setName("ROLE_ADMIN");
mongoTemplate.save(roleAdmin);
UserRole adminRole = new UserRole();
adminRole.setRole(roleAdmin);
admin.setUserRoles(new HashSet<>(Collections.singletonList(adminRole)));
mongoTemplate.save(admin);
}
@Test
void givenUserCredentials_whenInvokeUserAuthorizedEndPoint_thenReturn200() throws Exception {
mvc.perform(get("/user").with(httpBasic(USER_NAME, PASSWORD)))
.andExpect(status().isOk());
}
@Test
void givenUserNotExists_whenInvokeEndPoint_thenReturn401() throws Exception {
mvc.perform(get("/user").with(httpBasic("not_existing_user", "password")))
.andExpect(status().isUnauthorized());
}
@Test
void givenUserExistsAndWrongPassword_whenInvokeEndPoint_thenReturn401() throws Exception {
mvc.perform(get("/user").with(httpBasic(USER_NAME, "wrong_password")))
.andExpect(status().isUnauthorized());
}
@Test
void givenUserCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn403() throws Exception {
mvc.perform(get("/admin").with(httpBasic(USER_NAME, PASSWORD)))
.andExpect(status().isForbidden());
}
@Test
void givenAdminCredentials_whenInvokeAdminAuthorizedEndPoint_thenReturn200() throws Exception {
mvc.perform(get("/admin").with(httpBasic(ADMIN_NAME, PASSWORD)))
.andExpect(status().isOk());
mvc.perform(get("/user").with(httpBasic(ADMIN_NAME, PASSWORD)))
.andExpect(status().isOk());
}
}