diff --git a/pom.xml b/pom.xml index 186e802..50a5262 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ Demo project for Spring Boot - 11 + 1.8 @@ -29,6 +29,17 @@ spring-boot-starter-security + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + com.google.guava + guava + 28.1-jre + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/example/demo/auth/ApplicationUser.java b/src/main/java/com/example/demo/auth/ApplicationUser.java new file mode 100644 index 0000000..edc395d --- /dev/null +++ b/src/main/java/com/example/demo/auth/ApplicationUser.java @@ -0,0 +1,70 @@ +package com.example.demo.auth; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public class ApplicationUser implements UserDetails { + + private final Set grantedAuthorities; + private final String password; + private final String username; + private final boolean isAccountNonExpired; + private final boolean isAccountNonLocked; + private final boolean isCredentialsNonExpired; + private final boolean isEnabled; + + public ApplicationUser(String username, + String password, + Set grantedAuthorities, + boolean isAccountNonExpired, + boolean isAccountNonLocked, + boolean isCredentialsNonExpired, + boolean isEnabled) { + this.grantedAuthorities = grantedAuthorities; + this.password = password; + this.username = username; + this.isAccountNonExpired = isAccountNonExpired; + this.isAccountNonLocked = isAccountNonLocked; + this.isCredentialsNonExpired = isCredentialsNonExpired; + this.isEnabled = isEnabled; + } + + @Override + public Collection getAuthorities() { + return grantedAuthorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return isAccountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return isAccountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return isCredentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return isEnabled; + } +} diff --git a/src/main/java/com/example/demo/auth/ApplicationUserDAO.java b/src/main/java/com/example/demo/auth/ApplicationUserDAO.java new file mode 100644 index 0000000..a277a62 --- /dev/null +++ b/src/main/java/com/example/demo/auth/ApplicationUserDAO.java @@ -0,0 +1,9 @@ +package com.example.demo.auth; + +import java.util.Optional; + +public interface ApplicationUserDAO { + + Optional selectApplicationUserByUsername(String username); + +} diff --git a/src/main/java/com/example/demo/auth/ApplicationUserService.java b/src/main/java/com/example/demo/auth/ApplicationUserService.java new file mode 100644 index 0000000..b439f62 --- /dev/null +++ b/src/main/java/com/example/demo/auth/ApplicationUserService.java @@ -0,0 +1,25 @@ +package com.example.demo.auth; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +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; + +@Service +public class ApplicationUserService implements UserDetailsService { + + private final ApplicationUserDAO applicationUserDAO; + + @Autowired + public ApplicationUserService(@Qualifier("fake") ApplicationUserDAO applicationUserDAO){ + this.applicationUserDAO = applicationUserDAO; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return applicationUserDAO.selectApplicationUserByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException(String.format("Username %s not found", username))); + } +} diff --git a/src/main/java/com/example/demo/auth/FakeApplicationUserDaoService.java b/src/main/java/com/example/demo/auth/FakeApplicationUserDaoService.java new file mode 100644 index 0000000..8193bf8 --- /dev/null +++ b/src/main/java/com/example/demo/auth/FakeApplicationUserDaoService.java @@ -0,0 +1,62 @@ +package com.example.demo.auth; + +import com.example.demo.security.ApplicationUserRole; +import com.google.common.collect.Lists; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository("fake") +public class FakeApplicationUserDaoService implements ApplicationUserDAO { + + private final PasswordEncoder passwordEncoder; + + @Autowired + public FakeApplicationUserDaoService(PasswordEncoder passwordEncoder) { + this.passwordEncoder = passwordEncoder; + } + + @Override + public Optional selectApplicationUserByUsername(String username) { + return getApplicationUsers() + .stream() + .filter(applicationUser -> username.equals(applicationUser.getUsername())) + .findFirst(); + } + + private List getApplicationUsers() { + List applicationUsers = Lists.newArrayList( + new ApplicationUser( + "annasmith", + passwordEncoder.encode("pass"), + ApplicationUserRole.STUDENT.getGrantedAuthorities(), + true, + true, + true, + true + ), + new ApplicationUser( + "linda", + passwordEncoder.encode("pass"), + ApplicationUserRole.ADMIN.getGrantedAuthorities(), + true, + true, + true, + true + ), + new ApplicationUser( + "tom", + passwordEncoder.encode("pass"), + ApplicationUserRole.ADMINTRAINEE.getGrantedAuthorities(), + true, + true, + true, + true + ) + ); + return applicationUsers; + } +} diff --git a/src/main/java/com/example/demo/contoller/TemplateController.java b/src/main/java/com/example/demo/contoller/TemplateController.java new file mode 100644 index 0000000..c54f1bd --- /dev/null +++ b/src/main/java/com/example/demo/contoller/TemplateController.java @@ -0,0 +1,20 @@ +package com.example.demo.contoller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/") +public class TemplateController { + + @GetMapping("login") + public String getLogin() { + return "login"; + } + + @GetMapping("courses") + public String getCourses() { + return "courses"; + } +} diff --git a/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java b/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java index f653c63..b316ebf 100644 --- a/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java +++ b/src/main/java/com/example/demo/security/ApplicationSecurityConfig.java @@ -1,24 +1,93 @@ package com.example.demo.security; +import com.example.demo.auth.ApplicationUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +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.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.http.Cookie; +import java.net.CookieStore; +import java.util.concurrent.TimeUnit; + +import static com.example.demo.security.ApplicationUserRole.*; + @Configuration @EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter { + private final PasswordEncoder passwordEncoder; + private final ApplicationUserService applicationUserService; + + @Autowired + public ApplicationSecurityConfig(PasswordEncoder passwordEncoder, ApplicationUserService applicationUserService) { + this.applicationUserService = applicationUserService; + this.passwordEncoder = passwordEncoder; + } + @Override protected void configure(HttpSecurity http) throws Exception { http + .csrf().disable() .authorizeRequests() - .antMatchers("/", "index", "/css/*", "/js/*") - .permitAll() + .antMatchers("/", "index", "/css/*", "/js/*").permitAll() + .antMatchers("/api/**").hasRole(STUDENT.name()) .anyRequest() .authenticated() .and() - .httpBasic(); + .formLogin() + .loginPage("/login") + .permitAll() + .defaultSuccessUrl("/courses", true) + .passwordParameter("password") + .usernameParameter("username") + .and() + .rememberMe() + .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(21)) + .key("somethingverysecured") + .rememberMeParameter("remember-me") + .and() + .logout() + .logoutUrl("/logout") + .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")) // https://docs.spring.io/spring-security/site/docs/4.2.12.RELEASE/apidocs/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html + .clearAuthentication(true) + .invalidateHttpSession(true) + .deleteCookies("JSESSIONID", "remember-me") + .logoutSuccessUrl("/login"); } + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.authenticationProvider(daoAuthenticationProvider()); + } + + @Bean + public DaoAuthenticationProvider daoAuthenticationProvider() { + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(passwordEncoder); + provider.setUserDetailsService(applicationUserService); + return provider; + } + + + + public static void main(String[] args) { + System.out.println(TimeUnit.DAYS.toSeconds(1)); + } } diff --git a/src/main/java/com/example/demo/security/ApplicationUserPermission.java b/src/main/java/com/example/demo/security/ApplicationUserPermission.java new file mode 100644 index 0000000..3f6102b --- /dev/null +++ b/src/main/java/com/example/demo/security/ApplicationUserPermission.java @@ -0,0 +1,18 @@ +package com.example.demo.security; + +public enum ApplicationUserPermission { + STUDENT_READ("student:read"), + STUDENT_WRITE("student:write"), + COURSE_READ("course:read"), + COURSE_WRITE("course:write"); + + private final String permission; + + ApplicationUserPermission(String permission) { + this.permission = permission; + } + + public String getPermission() { + return permission; + } +} diff --git a/src/main/java/com/example/demo/security/ApplicationUserRole.java b/src/main/java/com/example/demo/security/ApplicationUserRole.java new file mode 100644 index 0000000..4d40004 --- /dev/null +++ b/src/main/java/com/example/demo/security/ApplicationUserRole.java @@ -0,0 +1,34 @@ +package com.example.demo.security; + +import com.google.common.collect.Sets; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Set; +import java.util.stream.Collectors; + +import static com.example.demo.security.ApplicationUserPermission.*; + +public enum ApplicationUserRole { + STUDENT(Sets.newHashSet()), + ADMIN(Sets.newHashSet(COURSE_READ, COURSE_WRITE, STUDENT_READ, STUDENT_WRITE)), + ADMINTRAINEE(Sets.newHashSet(COURSE_READ, STUDENT_READ)); + + private final Set permissions; + + ApplicationUserRole(Set permissions) { + this.permissions = permissions; + } + + public Set getPermissions() { + return permissions; + } + + public Set getGrantedAuthorities() { + Set permissions = getPermissions().stream() + .map(permission -> new SimpleGrantedAuthority(permission.getPermission())) + .collect(Collectors.toSet()); + permissions.add(new SimpleGrantedAuthority("ROLE_" + this.name())); + return permissions; + } +} diff --git a/src/main/java/com/example/demo/security/PasswordConfig.java b/src/main/java/com/example/demo/security/PasswordConfig.java new file mode 100644 index 0000000..0d9273b --- /dev/null +++ b/src/main/java/com/example/demo/security/PasswordConfig.java @@ -0,0 +1,15 @@ +package com.example.demo.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(10); + } +} diff --git a/src/main/java/com/example/demo/student/Student.java b/src/main/java/com/example/demo/student/Student.java index cd3cee3..35b1c9a 100644 --- a/src/main/java/com/example/demo/student/Student.java +++ b/src/main/java/com/example/demo/student/Student.java @@ -18,4 +18,12 @@ public Integer getStudentId() { public String getStudentName() { return studentName; } + + @Override + public String toString() { + return "Student{" + + "studentId=" + studentId + + ", studentName='" + studentName + '\'' + + '}'; + } } diff --git a/src/main/java/com/example/demo/student/StudentManagementController.java b/src/main/java/com/example/demo/student/StudentManagementController.java new file mode 100644 index 0000000..a832520 --- /dev/null +++ b/src/main/java/com/example/demo/student/StudentManagementController.java @@ -0,0 +1,48 @@ +package com.example.demo.student; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("management/api/v1/students") +public class StudentManagementController { + + private static final List STUDENTS = Arrays.asList( + new Student(1, "James Bond"), + new Student(2, "Maria Jones"), + new Student(3, "Anna Smith") + ); + +// hasRole('ROLE_') hasAnyRole('ROLE_') hasAuthority('permission') hasAnyAuthority('permission') + + @GetMapping + @PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_ADMINTRAINEE')") + public List getAllStudents() { + System.out.println("getAllStudents"); + return STUDENTS; + } + + @PostMapping + @PreAuthorize("hasAuthority('student:write')") + public void registerNewStudent(@RequestBody Student student) { + System.out.println("registerNewStudent"); + System.out.println(student); + } + + @DeleteMapping(path = "{studentId}") + @PreAuthorize("hasAuthority('student:write')") + public void deleteStudent(@PathVariable("studentId") Integer studentId) { + System.out.println("deleteStudent"); + System.out.println(studentId); + } + + @PutMapping(path = "{studentId}") + @PreAuthorize("hasAuthority('student:write')") + public void updateStudent(@PathVariable("studentId") Integer studentId, @RequestBody Student student) { + System.out.println("updateStudent"); + System.out.println(String.format("%s %s", studentId, student)); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b13789..164f752 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,2 @@ +server.port=8085 diff --git a/src/main/resources/templates/courses.html b/src/main/resources/templates/courses.html new file mode 100644 index 0000000..1649526 --- /dev/null +++ b/src/main/resources/templates/courses.html @@ -0,0 +1,22 @@ + + + + + + + Amigoscode login + + + + +
+

Courses

+

Spring Boot Security

+ +
+ + + diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..2d7b41c --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,40 @@ + + + + + + + Amigoscode login + + + + + +
+ +
+ + \ No newline at end of file