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:
| @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:
| 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;
}
}
|
- 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.
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";
}
}
|
| 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:
| <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ó:
Utolsó frissítés: 2021-10-14 13:07:51