Kihagyás

Spring Security

Auditing

Mielőtt az igazán komoly dolgokba belemennénk, nyúljunk vissza egy szösszenetre az Auditing-hoz. A korábbi kezdetleges konstans értéket visszaadó auditProvider-ünket okosítsuk fel az alábbiak szerint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditProvider")
public class JpaAuditConfig {

    @Bean
    public AuditorAware<String> auditProvider() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication().getName());
    }

}

Security

Modellek

  • Fussuk át az előző órán létrehozott ContactUser és Role osztályokat.
  • Emlékezzünk meg a privilégiumokról, azaz, hogy lehetséges lenne akár pl. CRUD művelet szinten szabályozni a jogosultságokat.
  • Oldjuk meg, hogy a ContactUser osztály getAuthorities() metódusa a valós role-ok alapján dolgozzon:
    1
    2
    3
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return roles.stream().map(r -> new SimpleGrantedAuthority(r.getName())).collect(Collectors.toList());
        }
    
  • Esetleg hozzáadhatunk a ContactUser osztályhoz egy boolean enabled és egy @Email String email adattagot.
  • A Role-ban pedig csináljunk egy paraméter nélküli és egy paraméteres konstruktort, amely nevet vár. Ezek a későbbiekben még jól jöhetnek.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    @Entity
    @Data
    @NoArgsConstructor
    public class Role {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        String name;
    
        public Role(String name) {
            this.name = name;
        }
    
    }
    

Repository-k

Hozzuk létre a ContactUser és a Role modellekhez tartozó repository-kat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package hu.suaf.contacts.repository;

import hu.suaf.contacts.model.ContactUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ContactUserRepository extends JpaRepository<ContactUser, Long> {

    ContactUser findByUsername(String username);

    ContactUser findByEmail(String email);

}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package hu.suaf.contacts.repository;

import hu.suaf.contacts.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {

    Role findByName(String name);

}

Teszt adatok

Tegyünk egy rövid kitekintést az adatbázisunk teszt adatokkal történő feltöltésének irányába. Alapvetően a data.sql megközelítést alkalmazva fel tudjuk tölteni a felhasználókkal és jogosultságokkal kapcsolatos táblákat is. Azonban például encode-olt jelszavakat nem túl egyszerű és elegáns ezzel a módszerrel megadni, sőt kompatibilitási gondok is felmerülhetnek.

Nézzük meg, hogy hogyan lehet ezt a feladatot ApplicationListener-eken keresztül megoldani. Hozzuk létre a config package-ben az alábbi UserDataSetup osztályt, amelynek mindösszesen annyi a feladata, hogy létrehoz egy USER és egy ADMIN role-t, valamint egy adminisztrátori jogokkal rendelkező felhasználót és elmenti ezeket az adatbázisba.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package hu.suaf.contacts.config;

import hu.suaf.contacts.model.ContactUser;
import hu.suaf.contacts.model.Role;
import hu.suaf.contacts.repository.ContactUserRepository;
import hu.suaf.contacts.repository.RoleRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Component
@Slf4j
public class UserDataSetup implements ApplicationListener<ContextRefreshedEvent> {

    private ContactUserRepository userRepository;
    private RoleRepository roleRepository;
    private PasswordEncoder passwordEncoder;

    boolean alreadySetup = false;

    @Autowired
    public UserDataSetup(ContactUserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    @Transactional
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (alreadySetup) {
            return;
        }

        Role adminRole = createRoleIfNotExists("ADMIN");
        Role userRole = createRoleIfNotExists("USER");

        ContactUser user = new ContactUser();
        user.setEnabled(true);
        user.setRoles(List.of(adminRole));
        user.setPassword(passwordEncoder.encode("asdqwe"));
        user.setUsername("test");
        user.setEmail("test@test.com");

        userRepository.save(user);

        alreadySetup = true;

        log.info("test user saved");
    }

    @Transactional
    Role createRoleIfNotExists(String name) {
        Role role = roleRepository.findByName(name);

        if (role == null) {
            role = new Role(name);
            roleRepository.save(role);
        }

        return role;
    }
}

UserDetailsService

Készítsük el a ContactUserDetailsService osztályt, amely a Spring-es UserDetailsService interfészt implementálja és a felhasználókkal kapcsolatos műveletekért lesz felelős. Első körben gondoskodjunk az injektálásokról és implementáljuk a loadUserByUsername(String s) metódust.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package hu.suaf.contacts.service;


import hu.suaf.contacts.model.ContactUser;
import hu.suaf.contacts.model.Role;
import hu.suaf.contacts.repository.ContactUserRepository;
import hu.suaf.contacts.repository.RoleRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;

@Service("userDetailsService")
@Transactional
@Slf4j
public class ContactUserDetailsService implements UserDetailsService {

    private ContactUserRepository userRepository;
    private RoleRepository roleRepository;

    @Autowired
    public ContactUserDetailsService(ContactUserRepository userRepository, RoleRepository roleRepository) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
        ContactUser user = userRepository.findByUsername(usernameOrEmail);

        if(user == null){
            user = userRepository.findByEmail(usernameOrEmail);

            if(user == null){
                throw new UsernameNotFoundException("Could not find user with username (or email): " + usernameOrEmail);
            }
        }

        log.info(user.getUsername() + " found");
        return user;
    }
}

Egyedi login form

  • Próbáljuk ki az alkalmazásunkat a teljesen default security konfigurációval, vagyis az auth és a http configure részeket vagy hagyjuk el, vagy hívjuk az ősosztály megfelelő metódusait.

Tipp: Amennyiben esetleg lazy init-tel kapcsolatos hibát kapnánk, akkor javítsuk a ContactUser megfelelő kapcsolatának definícióját: @ManyToMany(fetch = FetchType.EAGER)

HTTP konfiguráció

Készítsünk egy szofisztikáltabb http konfigurációt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/register").permitAll()
                .antMatchers("/css/**", "/images/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login").permitAll()
                .successForwardUrl("/contact")
                .and()
                .logout().permitAll()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login");
    }

A successForwardUrl POST-ot fog küldeni, így a ContactController-ben a listOfContacts-ot át kell állítsuk úgy, hogy GET és POST fogadására is alkalmas legyen.

Form-ok

Valamint hozzuk létre az ehhez szükséges login.html és register.html form-okat és Controller-jeiket.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<div th:replace="fragments/header :: header (title='Login')"></div>
<body>

<div class="container">
    <h1>Please log in</h1>
    <!--/*@thymesVar id="contactUser" type="hu.suaf.contacts.model.ContactUser"*/-->
    <form th:action="@{/login}" th:object="${contactUser}" method="post" novalidate>
        <div class="form-group">
            <label for="username">Username</label>
            <input type="text" class="form-control" id="username" th:field="*{username}">
            <span class="validationError" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></span>
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" th:field="*{password}">
            <span class="validationError" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></span>
        </div>
        <button type="submit" class="btn btn-primary" th:text="Login">Submit</button>
        <a th:href="@{/register}" class="btn btn-secondary">Register</a>
    </form>

</div>
</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<div th:replace="fragments/header :: header (title='Register')"></div>

<body>

<div class="container">
    <h1>Register</h1>
    <!--/*@thymesVar id="contactUser" type="hu.suaf.contacts.model.ContactUser"*/-->
    <form  th:action="@{/register}" th:object="${contactUser}" method="post" novalidate>
        <div class="form-group">
            <label for="username">Username</label>
            <input type="text" class="form-control" id="username" th:field="*{username}">
            <span class="validationError" th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></span>
        </div>
        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" class="form-control" id="email" th:field="*{email}">
            <span class="validationError" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></span>
        </div>
        <div class="form-group">
            <label for="password">Password</label>
            <input type="password" class="form-control" id="password" th:field="*{password}">
            <span class="validationError" th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></span>
        </div>
        <button type="submit" class="btn btn-primary">Register</button>
    </form>
</div>

</body>
</html>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package hu.suaf.contacts.controller;

import hu.suaf.contacts.model.ContactUser;
import hu.suaf.contacts.service.ContactUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.validation.Valid;

@Controller
public class AuthController {

    private ContactUserDetailsService userDetailsService;

    @Autowired
    public void setUserDetailsService(ContactUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @GetMapping("/login")
    public String showLoginForm(Model model, ContactUser contactUser, @RequestParam(required = false) String error) {
        if (error != null) {
            model.addAttribute("loginError", true);
            model.addAttribute("contactUser", contactUser);
        }

        return "login";
    }

    @GetMapping("/register")
    public String showRegisterForm(ContactUser contactUser) {
        return "register";
    }

    @PostMapping("/register")
    public String register(@Valid ContactUser contactUser, BindingResult result) {
        if (result.hasErrors()) {
            return "register";
        }

        userDetailsService.registerUser(contactUser);

        return "redirect:/login";
    }

}
1
2
3
4
5
6
7
8
9
    public void registerUser(ContactUser user) {
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setRoles(List.of(roleRepository.findByName("USER")));
        user.setEnabled(true);

        userRepository.save(user);

        log.info("user registered: " + user);
    }

Thymeleaf Security

A Thymeleaf biztosít számunkra néhány olyan funkciót, amely jelentősen megkönnyíti a security aspektusok kezelését.

Először is szükségünk lesz az alábbi függőségre:

1
2
3
4
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

A következő lépésben meg kell adjunk egy új XML namespace-t (xmlns:sec="http://www.thymeleaf.org/extras/spring-security") amellyel behúzzuk a Thymeleaf extra funkcióit. Majd nincs más dolgunk, mint az új funkciókat használva például a belépett felhasználók számára megjelenítani a logout opciót, valamint csak adminisztrátorok számára elérhetővé tenni a felhasználók kezelését a /users URL-en.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<nav class="navbar navbar-expand-lg navbar-light bg-light" th:fragment="menu(current)" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
  <a class="navbar-brand" href="#"><img class="logo" th:src="@{/images/contacts_logo.png}"></a>
  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
  </button>
  <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
    <div class="navbar-nav">
      <a class="nav-item nav-link" th:classappend="${current} == 'Create' ? 'active' : ''" th:href="@{/contact/create}">Create</a>
      <a class="nav-item nav-link" th:classappend="${current} == 'List' ? 'active' : ''" th:href="@{/contact}">List</a>
      <a sec:authorize="hasAnyAuthority('ADMIN')" class="nav-item nav-link" th:href="@{/users}">User</a>
    </div>
    <ul class="navbar-nav ml-auto">
      <li class="nav-item">
        <a sec:authorize="isAuthenticated()" class="nav-item nav-link" th:href="@{/logout}">Logout</a>
      </li>
    </ul>
  </div>
</nav>

Figyelem

Egy menüpontnak a view-n történő meg nem jelenítése még vajmi kevés védelmet nyújt, ugyanis ha a felhasználó véletlen kitalálja az URL-t, akkor szabadon garázdálkodhat. Tehát mindenképpen összhangba kell hozzuk a view-n és a security config-ban alkalmazott megszorításokat. A /users menüpont esetében például be kell szúrjuk a következőt a megfelelő configure metódusba: .antMatchers("/users").hasAnyAuthority("ADMIN")

Feladat

Valósítsd meg a felhasználók kezelésére szolgáló admin felületet.

Videó

A gyakorlat anyagáról készült videó:

SUAF_04_gyak


Utolsó frissítés: 2021-10-14 13:07:51
Back to top