Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Form認証で学ぶSpring Security入門

Form認証で学ぶSpring Security入門

JSUG勉強会 2019その8 Spring for Beginner #jsug

Ryosuke Uchitate

August 28, 2019
Tweet

More Decks by Ryosuke Uchitate

Other Decks in Programming

Transcript

  1. 4FDVSJUZ'JMUFSͷྫ w -PHPVU'JMUFSKBWB w ϩάΞ΢τॲཧΛߦ͏ w 6TFSOBNF1BTTXPSE"VUIFOUJDBUJPO'JMUFSKBWB w 'PSNೝূͰೝূॲཧΛߦ͏ w

    #BTJD"VUIFOUJDBUJPO'JMUFSKBWB w ϕʔγοΫೝূͰೝূॲཧΛߦ͏ w &YDFQUJPO5SBOTMBUJPO'JMUFSKBWB w ೝՄॲཧͰൃੜͨ͠ྫ֎ΛϋϯυϦϯά͠ɺΫϥΠΞϯτ΁
 ద੾ͳϨεϙϯεΛߦ͏
  2. ࢓༷ w ϖʔδͷࢀরʹ͸ೝূ͕ඞਢ w ݖݶ͸ɺ08/&3."/"(&345"'' ొ࿥ Ұཡɾৄࡉ ࡟আ 08/&3 Մ

    Մ Մ ."/"(&3 Մ MANAGERͱSTAFF ͷΈӾཡՄ ෆՄ 45"'' ෆՄ STAFFͷΈӾཡՄ ෆՄ
  3. ࣮૷͢ΔΫϥε w 8FC4FDVSJUZ$POpHKBWB w 4QSJOH4FDVSJUZ༻ͷઃఆ w "VUIFOUJDBUFE6TFSKBWB w 6TFS%FUBJMTΠϯλʔϑΣʔεͷ࣮૷ɻೝূࡁͷϢʔβ৘ใ w

    6TFS%FUBJMT.BOBHFSKBWB w 6TFS%FUBJMT4FSWJDFΠϯλʔϑΣʔεͷ࣮૷ɻࢿ֨৘ใͱϢʔβͷঢ়ଶΛ σʔλετΞ͔Βऔಘ
  4. 8FC4FDVSJUZ$POpHKBWB @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private

    final UserDetailsManager userDetailsManager; public WebSecurityConfig(UserDetailsManager userDetailsManager) { this.userDetailsManager = userDetailsManager; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/users/create").hasRole("OWNER", "MANAGER") .antMatchers("/users/delete/{id}").hasRole("OWNER") .anyRequest().authenticated() .and().formLogin().loginPage("/login").defaultSuccessUrl("/users", true) .and().logout().logoutSuccessUrl("/login").permitAll() .and().csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsManager) .passwordEncoder(passwordEncoder()); } }
  5. 8FC4FDVSJUZ$POpHKBWBʢղઆʣ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests()

    // "/users/create"͸OWNERͱMANAGERݖݶͷΈϦΫΤετՄ .antMatchers("/users/create").hasAnyRole("OWNER", "MANAGER") // "/users/delete/{id}"͸OWNERͱMANAGERݖݶͷΈϦΫΤετՄ .antMatchers(“/users/delete/{id}").hasRole("OWNER") // ೝূࡁΈϢʔβͷΈ͕ϦΫΤετՄ .anyRequest().authenticated() // FORMೝূΛ༗ޮʹ͢ΔɺϩάΠϯϖʔδͷύε͸"/login"ɺϩάΠϯ੒ޭޙ͸"/users"ʹϦμΠ ϨΫτ .and().formLogin().loginPage("/login").defaultSuccessUrl("/users", true) // ϩάΞ΢τ੒ޭޙͷϦμΠϨΫτઌ͸"/login" .and().logout().logoutSuccessUrl("/login").permitAll() // CSRFରࡦػೳΛແޮʹ͢Δ .and().csrf().disable(); }
  6. 'PSN-PHJO$POpHVSFSKBWB public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>,

    UsernamePasswordAuthenticationFilter> { public FormLoginConfigurer() { // UsernamePasswordAuthenticationFilterΛAuthenticationFilterʹઃఆ super(new UsernamePasswordAuthenticationFilter(), null); // Formͷusernameύϥϝʔλ໊Λ"username"ʹ usernameParameter(“username"); // Formͷpasswordύϥϝʔλ໊Λ"password"ʹ passwordParameter("password"); } 'PSN-PHJO$POpHVSFSKBWBΛOFXͯ͠ '03.ೝূΛ༗ޮʹ͢Δ
  7. 8FC4FDVSJUZ$POpHKBWBʢղઆʣ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //

    ೝূ࣌ʹ࢖͏UserDetailsService.javaͷ࣮૷Λઃఆɺར༻͢ΔPasswordEncoder.javaΛઃఆ auth.userDetailsService(userDetailsManager) .passwordEncoder(passwordEncoder()); }
  8. 6TFSOBNF1BTTXPSE"VUIFOUJDBUJPO'JMUFSKBWB public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

    if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // ϦΫΤετύϥϝʔλ͔Βೖྗ஋Λநग़ String username = obtainUsername(request); String password = obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); // AuthenticationManagerʹࢿ֨৘ใΛ౉͢ return this.getAuthenticationManager().authenticate(authRequest); }
  9. %BP"VUIFOUJDBUJPO1SPWJEFSKBWB protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException

    { prepareTimingAttackProtection(); try { // Ϣʔβ৘ใΛऔಘ UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } }
  10. %BP"VUIFOUJDBUJPO1SPWJEFSKBWB protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {

    if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); // ύεϫʔυͷൺֱ if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug( "Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials”)); } }
  11. 6TFS%FUBJMT.BOBHFSKBWB @Service public class UserDetailsManager implements UserDetailsService { private final

    UserRepository userRepository; public UserDetailsManager(UserRepository userRepository) { this.userRepository = userRepository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepository.findByUsername(username) .map(AuthenticatedUser::new) .orElseThrow( () -> new UsernameNotFoundException("username not found")); } }
  12. "VUIFOUJDBUFE6TFSKBWB public class AuthenticatedUser implements UserDetails { private final Integer

    id; private final String name; private final String username; private final String password; private final Role role; // ίϯετϥΫλ͸লུ public Integer getId() { return id; } public String getName() { return name; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return createAuthorityList("ROLE_" + role.name()); } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
  13. 8FC4FDVSJUZ$POpHKBWB @Bean public RoleHierarchy roleHierarchy(SecurityRolesProperties rolesProperties) { return rolesProperties.getRoleHierarchy(); }

    @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/users/create").hasRole("MANAGER") .antMatchers("/users/delete/{id}").hasRole("OWNER") .anyRequest().authenticated() .and() .formLogin().loginPage("/login").defaultSuccessUrl("/users", true) .and() .logout().logoutSuccessUrl("/login").permitAll() .and() .csrf().disable(); }
  14. 8FC4FDVSJUZ$POpHKBWBʢղઆʣ @Bean public RoleHierarchy roleHierarchy(SecurityRolesProperties rolesProperties) { // RoleHierarchyImpl.javaΛBeanొ࿥ return

    rolesProperties.getRoleHierarchy(); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // ԼͷRoleΛؚΈ࣋ͭͷͰMANAGERͷΈͰΑ͍ .antMatchers("/users/create").hasRole("MANAGER") .antMatchers("/users/delete/{id}").hasRole("OWNER") .anyRequest().authenticated() .and() .formLogin().loginPage("/login").defaultSuccessUrl("/users", true) .and() .logout().logoutSuccessUrl("/login").permitAll() .and() .csrf().disable(); }
  15. 4FDVSJUZ3PMFT1SPQFSUJFTKBWB @Component @ConfigurationProperties("security.roles") public class SecurityRolesProperties { private Map<String, List<String>>

    hierarchyMap = new LinkedHashMap<>(); public Map<String, List<String>> getHierarchyMap() { return hierarchyMap; } public void setHierarchyMap(Map<String, List<String>> hierarchyMap) { this.hierarchyMap = hierarchyMap; } public RoleHierarchy getRoleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = isEmpty(hierarchyMap) ? "" : roleHierarchyFromMap(hierarchyMap); roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; } }
  16. 4FDVSJUZ3PMF1SPQFSUJFTKBWBʢղઆʣ @Component @ConfigurationProperties("security.roles") public class SecurityRolesProperties { private Map<String, List<String>>

    hierarchyMap = new LinkedHashMap<>(); public Map<String, List<String>> getHierarchyMap() { return hierarchyMap; } public void setHierarchyMap(Map<String, List<String>> hierarchyMap) { this.hierarchyMap = hierarchyMap; } public RoleHierarchy getRoleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); String hierarchy = isEmpty(hierarchyMap) ? "" : roleHierarchyFromMap(hierarchyMap); // HierarchyΛจࣈྻͰηοτ͢Δ // hierarchy = "ROLE_OWNER > ROLE_MANAGER\nROLE_MANAGER > ROLE_STAFF\n" roleHierarchy.setHierarchy(hierarchy); return roleHierarchy; } }
  17. !1SF"VUIPSJ[F // ϩάΠϯϢʔβͷRole͕OWNERͷͱ͖ϝιουʹೖΕΔ @PreAuthorize("hasRole('OWNER')") public List<User> list() { return userRepository.findAll();

    } // Ҿ਺role͕"OWNER"ʹҰக͢Δͱ͖ϝιουʹೖΕΔ @PreAuthorize("#role == 'OWNER'") public List<User> list(String role) { return userRepository.findAll(); } // Ҿ਺request.name͕"ruchitate"ʹҰக͢Δͱ͖ϝιουʹೖΕΔ @PreAuthorize("#r.name == 'ruchitate'") public List<User> list(@P("r") UserRequest request) { return userRepository.findAll(); } ϝιουΛݺ΂ΔݖݶΛ͍࣋ͬͯΔ͔
  18. !1PTU"VUIPSJ[F // ໭Γ஋ͷม਺໊͸σϑΥϧτͰreturnObject @PostAuthorize("returnObject != null && returnObject.username == 'ruchitate'")

    public User get(Integer id) { return userRepository.findById(id).orElse(null); } औಘͰ͖ΔݖݶΛ͍࣋ͬͯΔ͔
  19. !1SF'JMUFS // Ҿ਺ͷม਺໊͸σϑΥϧτͰfilterObject @PreFilter("filterObject.name.equals('ruchitate')") public List<User> list(List<UserRequest> requests) { List<String>

    usernameList = requests.stream() .map(UserRequest::getName) .collect(Collectors.toList()); return userRepository.findAllByUsernameIn(usernameList); } Ҿ਺ͷதͰ৚݅ʹҰக͢ΔΦϒδΣΫτͷΈநग़
  20. ࠓճͷαϯϓϧʹద༻͢Δ @Override // Beanొ࿥ͨ͠Ϋϥε΋࢖͑Δ @PostFilter("@roleEvaluator.hasRole(principal, filterObject.role)") public List<UserDto> findAll() {

    List<UserDto> userDtoList = new ArrayList<>(); userRepository.findAll().iterator() .forEachRemaining(user -> userDtoList.add(UserDto.newUserDto(user))); return userDtoList; } @Override @PostAuthorize("returnObject != null && @roleEvaluator.hasRole(principal, returnObject.role)") public UserDto findOne(Integer id) { return userRepository.findById(id) .map(UserDto::newUserDto) .get(); }
  21. ࠓճͷαϯϓϧʹద༻͢Δ @Override // MANAGER or OWNER @PreAuthorize("hasRole('MANAGER')") public UserDto create(UserCreateForm

    form) { User user = new User(); BeanUtils.copyProperties(form, user, "password"); user.setPassword(passwordEncoder.encode(form.getPassword())); return UserDto.newUserDto(userRepository.save(user)); } @Override @PreAuthorize("hasRole('OWNER')") public void delete(Integer id) { User user = userRepository.findById(id) .filter(u -> u.getRole() != Role.OWNER) .orElseThrow(NotAllowedOperationException::new); userRepository.delete(user); }
  22. 6TFS$POUSPMMFS5FTUKBWB @Nested @SpringBootTest class ListTest { @Autowired private WebApplicationContext context;

    private MockMvc mockMvc; @BeforeEach void beforeEach() { // SecurityFilterΛ௥Ճ mockMvc = webAppContextSetup(context).apply(springSecurity()).build(); } @Test void success() throws Exception { User user = new User(); user.setId(1); user.setName("಺ཱྑհ"); user.setUsername("ruchitate"); user.setPassword("12345678"); user.setRole(Role.OWNER); mockMvc.perform(get("/users") // ΞΫηε͢ΔϢʔβʔ .with(user(new AuthenticatedUser(user)))) .andExpect(status().isOk()); } }
  23. 6TFS4FSWJDF*NQM5FTUKBWB @Test // ΞΫηε͢ΔϢʔβ @WithUserDetails(value = "yaragaki", userDetailsServiceBeanName = "userDetailsManager")

    void findAllForManager() { List<UserDto> result = userService.findAll(); // ࣗ෼ͷRoleҎԼͷϢʔβͷΈࢀরՄೳ Assertions.assertThat(result) .extracting( UserDto::getId, UserDto::getName, UserDto::getAge, UserDto::getGender, UserDto::getRole) .containsExactly( Tuple.tuple(2, "৽֞ɹ݁ҥ", 31, WOMAN, MANAGER), Tuple.tuple(3, "ࢁ࡚ɹݡਓ", 24, MAN, STAFF)); }
  24. 6TFS4FSWJDF*NQM5FTUKBWB @Test @WithUserDetails(value = "ruchitate", userDetailsServiceBeanName = "userDetailsManager") void deleteForOwner()

    { userService.delete(2); Assertions.assertThatThrownBy(() -> userService.findOne(2)) .isInstanceOf(NotFoundException.class); } @Test @WithUserDetails(value = "yaragaki", userDetailsServiceBeanName = "userDetailsManager") void deleteForManager() { Assertions.assertThatThrownBy(() -> userService.delete(3)) .isInstanceOf(AccessDeniedException.class); } 08/&3ͷΈ࡟আՄ
  25. '03.ೝূͷςετ @Test void loginSuccess() throws Exception { MvcResult result //

    ϩάΠϯϢʔβΛઃఆ = mockMvc.perform(formLogin().user("ruchitate").password("12345678")) .andReturn(); Assertions.assertThat(result.getResponse()) .extracting( MockHttpServletResponse::getStatus, MockHttpServletResponse::getRedirectedUrl) .containsExactly(HttpStatus.FOUND.value(), "/users"); } @Test void loginFailed() throws Exception { MvcResult result = mockMvc.perform(formLogin().user("ruchitate").password("test")) .andReturn(); Assertions.assertThat(result.getResponse()) .extracting( MockHttpServletResponse::getStatus, MockHttpServletResponse::getRedirectedUrl) .containsExactly(HttpStatus.FOUND.value(), "/login?error"); }