Die Verwendung von Spring Security
Das initiale Projekt
Da Spring Security Teil von Spring Boot ist, legen wir zunächst ein neues Spring Boot Projekt an. Intellij bietet uns durch den `Spring Initializr` dabei Unterstützung an. Nach dem Ausfüllen diverser Dialogfenster besteht das initiale Projekt aus einer Startklasse, einer Testklasse, der `pom.xml` und der `application.properties`. Da ich in diesem Beitrag weder auf Tests eingehe, noch die Default-Einstellungen von Spring verändern werde, ignorieren wir die Testklasse und `application.properties` und widmen uns den zwei verbleibenden Dateien. Die Startklasse, die einen beliebigen Namen haben kann, besitzt die Annotation `@SpringBootApplication` und startet unsere Anwendung in einer main-Methode:
1
2
3
4
5
6
7
8
9
10
11
12
|
package de.micromata.spring.security.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringSecurityApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityApplication. class , args); } } |
|
1
2
3
4
|
< dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package de.micromata.spring.security.example.controlller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class DefaultController { @RequestMapping ( "/" ) public String index() { return "You can only see this, if you are logged in!" ; } @RequestMapping ( "/noSecurity" ) public String noSecurity() { return "Everybody can see this!" ; } } |
Einführung von Spring SecurityIn modernen Anwendungen ist Security ein wichtiger Faktor. Aus diesem Grund führen wir nun Spring Security in unsere Anwendung ein. Dazu binden wir zunächst folgende Abhängigkeit ein:
1
2
3
4
|
< dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-security</ artifactId > </ dependency > |
Außerdem legen wir eine Klasse an, die von `WebSecurityConfigurerAdapter` erbt:
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
|
package de.micromata.spring.security.example.securitiy.conf; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers( "/noSecurity" ).permitAll() .anyRequest().authenticated() .and().formLogin().permitAll(); } @Autowired public void globalSecurityConfiguration(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser( "user" ).password( "password" ).roles( "USER" ); auth.inMemoryAuthentication().withUser( "admin" ).password( "password" ).roles( "USER" , "ADMIN" ); } } |
Thymeleaf und Spring SecurityViele moderne Anwendungen stellen eigene Ansprüche an die Loginseite. Aus diesem Grund führen wir nun Thymleaf ein, um mit dessen Hilfe eine eigene Login Seite zu erstellen. Dazu binden wir zunächst folgende Abhängigkeit ein:
1
2
3
4
|
< dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-thymeleaf</ artifactId > </ dependency > |
1
2
3
4
5
6
7
|
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers( "/noSecurity" ).permitAll() .anyRequest().authenticated() .and().formLogin().loginPage( "/login" ).permitAll() .and().logout().permitAll(); } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package de.micromata.spring.security.example.controlller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class DefaultController { @RequestMapping ( "/" ) public String index() { return "index" ; } @RequestMapping ( "/login" ) public String login() { return "login" ; } @RequestMapping ( "/noSecurity" ) public String noSecurity() { return "noSecurity" ; } } |
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
|
`index.html`: < head th:replace = "fragments/headerAndNav :: header" /> < body > < div th:replace = "fragments/headerAndNav :: navbar" /> < div class = "container" > You can only see this, if you are logged in! </ div > </ body > </ html > `noSecurity.html`: < head th:replace = "fragments/headerAndNav :: header" /> < body > < div th:replace = "fragments/headerAndNav :: navbar" /> < div class = "container" > Everybody can see this! </ div > </ body > </ html > `login.html`: < head th:replace = "fragments/headerAndNav :: header" /> < body > < div th:replace = "fragments/headerAndNav :: navbar" /> < div class = "container" > < form name = "f" th:action = "@{/login}" method = "post" > < fieldset > < legend >Please Login</ legend > < div th:if = "${param.error}" class = "alert alert-error" > Invalid username and password. </ div > < div th:if = "${param.logout}" class = "alert alert-success" > You have been logged out. </ div > < div class = "form-group" > < label for = "username" >Username</ label > < input type = "text" id = "username" name = "username" /> </ div > < div class = "form-group" > < label for = "password" >Password</ label > < input type = "password" id = "password" name = "password" /> </ div > < button type = "submit" class = "btn btn-primary" >Log in</ button > </ fieldset > </ 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
|
< head th:fragment = "header" > < title tiles:fragment = "title" >Spring Security Example</ title > < link rel = "stylesheet" integrity = "sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin = "anonymous" /> </ head > < body > < nav class = "navbar navbar-inverse" th:fragment = "navbar" > < div class = "container-fluid" > < div class = "navbar-inner" th:with = "currentUser=${#httpServletRequest.userPrincipal?.name}" > < a class = "navbar-brand" th:href = "@{/}" >Home</ a > < a class = "navbar-brand" th:href = "@{/noSecurity}" >No Security</ a > < div th:if = "${currentUser != null}" > < form class = "navbar-form navbar-right" th:action = "@{/logout}" method = "post" > < input type = "submit" class = "btn btn-primary" value = "Log out" /> </ form > < p class = "navbar-text navbar-right" th:text = "${currentUser}" > example_user </ p > </ div > </ div > </ div > </ nav > </ body > </ html > |
1
2
3
4
5
6
7
8
9
10
11
|
< dependency > < groupId >org.thymeleaf</ groupId > < artifactId >thymeleaf</ artifactId > < version >3.0.7.RELEASE</ version > </ dependency > < dependency > < groupId >org.thymeleaf</ groupId > < artifactId >thymeleaf-spring4</ artifactId > < version >3.0.7.RELEASE</ version > </ dependency > |
JPA
Neben einer eigenen Loginseite besitzen heutige Webanwendungen für gewöhnlich auch eigene User. Aus diesem Grund führen wir nun JPA ein. Dazu binden wir folgende Abhängigkeiten ein:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency>
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
|
package de.micromata.spring.security.example.data; import org.hibernate.validator.constraints.NotEmpty; import javax.persistence.*; @Entity public class User { @Id @GeneratedValue () private long id; @NotEmpty (message = "username is required" ) @Column (unique = true ) private String username; @NotEmpty (message = "password is required" ) private String password; protected User() {} public User(String userName, String password) { this .username = userName; this .password = password; } public String getUsername() { return username; } public void setUsername(String username) { this .username = username; } public String getPassword() { return password; } public void setPassword(String password) { this .password = password; } } |
1
2
3
4
5
6
7
|
package de.micromata.spring.security.example.data; import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository<User, Long> { User findByUsername(String username); } |
1
2
3
4
5
6
7
|
@Autowired private UserDetailsService userDetailsService; @Autowired public void globalSecurityConfiguration(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } |
1
2
3
4
5
|
package org.springframework.security.core.userdetails; public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package org.springframework.security.core.userdetails; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.io.Serializable; import java.util.Collection; public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); } |
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
|
package de.micromata.spring.security.example.securitiy.user; import de.micromata.spring.security.example.data.User; import de.micromata.spring.security.example.data.UserRepository; 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.stereotype.Service; @Service public class AuthenticatedUserService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) { User user = userRepository.findByUsername(username); if (user == null ) { throw new UsernameNotFoundException( "The user " + username + " does not exist" ); } return new AuthenticatedUser(user); } } |
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
|
package de.micromata.spring.security.example.securitiy.user; import de.micromata.spring.security.example.data.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; public class AuthenticatedUser extends User implements UserDetails { protected AuthenticatedUser(User user) { super (user.getUsername(), user.getPassword()); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return AuthorityUtils.createAuthorityList( "ROLE_USER" ); } @Override public boolean isAccountNonExpired() { return true ; } @Override public boolean isAccountNonLocked() { return true ; } @Override public boolean isCredentialsNonExpired() { return true ; } @Override public boolean isEnabled() { return true ; } } |
1
2
|
insert into user (id,username, password ) values (0, 'user' , 'password' ); insert into user (id,username, password ) values (1, 'admin' , 'password' ); |
Rollen und Rechte User allein reichen für moderne Webanwendungen jedoch meistens nicht aus. Aus diesem Grund führen wir nun Rollen und Rechte ein. Für die Rollen haben wir ein entsprechendes Feld in unsere User-Entity hinzugefügt:
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
|
package de.micromata.spring.security.example.data; import org.hibernate.validator.constraints.NotEmpty; import javax.persistence.*; @Entity public class User { @Id @GeneratedValue () private long id; @NotEmpty (message = "username is required" ) @Column (unique = true ) private String username; @NotEmpty (message = "password is required" ) private String password; @NotEmpty (message = "role is required" ) private String role; protected User() {} public User( long id, String userName, String password, String role) { this .id = id; this .username = userName; this .password = password; this .role = role; } public String getUsername() { return username; } public void setUsername(String username) { this .username = username; } public String getPassword() { return password; } public void setPassword(String password) { this .password = password; } public String getRole() { return role; } public void setRole(String role) { this .role = role; } public long getId() { return id; } public void setId( long id) { this .id = id; } } |
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
|
package de.micromata.spring.security.example.securitiy.user; import de.micromata.spring.security.example.data.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; public class AuthenticatedUser extends User implements UserDetails { protected AuthenticatedUser(User user) { super (user.getId(), user.getUsername(), user.getPassword(), user.getRole()); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return AuthorityUtils.createAuthorityList(getRole()); } @Override public boolean isAccountNonExpired() { return true ; } @Override public boolean isAccountNonLocked() { return true ; } @Override public boolean isCredentialsNonExpired() { return true ; } @Override public boolean isEnabled() { return true ; } } |
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
|
package de.micromata.spring.security.example.data; import org.hibernate.validator.constraints.NotEmpty; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.OneToOne; import javax.validation.constraints.NotNull; @Entity public class Message { @Id @GeneratedValue () private long id; @OneToOne @NotNull private User user; @NotEmpty (message = "text is required" ) private String text; @NotEmpty (message = "title is required" ) private String title; public User getUser() { return user; } public void setUser(User user) { this .user = user; } public String getText() { return text; } public void setText(String text) { this .text = text; } public String getTitle() { return title; } public void setTitle(String title) { this .title = title; } public long getId() { return id; } public void setId( long id) { this .id = id; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
package de.micromata.spring.security.example.data; import org.springframework.data.repository.CrudRepository; import org.springframework.security.access.prepost.PostAuthorize; public interface MessageRepository extends CrudRepository<Message, Long> { @PostAuthorize ( "hasPermission(returnObject, 'message')" ) Message findById(Long id); Iterable<Message> findByUserId(Long id); @PostAuthorize ( "hasPermission(returnObject, 'privateMessage')" ) Message findOne(Long id); } |
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
|
package de.micromata.spring.security.example.securitiy.permission; import de.micromata.spring.security.example.data.Message; import de.micromata.spring.security.example.data.User; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.io.Serializable; @Component public class SimplePermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { if (authentication == null ) { return false ; } Message message = (Message) targetDomainObject; if (message == null ) { return true ; } User user = (User) authentication.getPrincipal(); if (user.getId() == message.getUser().getId()) { return true ; } if ( "privateMessage" .equals(permission)) { return false ; } else if ( "message" .equals(permission) && "ROLE_ADMIN" .equals(user.getRole())) { return true ; } else { return false ; } } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { return false ; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@RequestMapping ( "/messages" ) public String listMessages( @AuthenticationPrincipal User user, Model model) { Iterable<Message> messages = messageRepository.findByUserId(user.getId()); model.addAttribute( "messages" , messages); return "listMessages" ; } @RequestMapping (value = "/message/{id}" ) public String viewMessage( @PathVariable Long id, Model model) { Message message = messageRepository.findById(id); model.addAttribute( "message" , message); return "viewMessage" ; } @RequestMapping ( "/privateMessage/{id}" ) public String viewPrivateMessage( @PathVariable Long id, Model model) { Message message = messageRepository.findOne(id); model.addAttribute( "message" , message); return "viewMessage" ; } |
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
|
`listMessages.html`: < head th:replace = "fragments/headerAndNav :: header" /> < body > < div th:replace = "fragments/headerAndNav :: navbar" /> < div class = "container" > < ul th:each = "message : ${messages}" > < li >< a href = "viewMessage.html" th:href = "@{'/message/' + ${message.id}}" th:text = "${message.title}" >The title</ a ></ li > </ ul > </ div > </ body > </ html > `viewMessages.html`: < head th:replace = "fragments/headerAndNav :: header" /> < body > < div th:replace = "fragments/headerAndNav :: navbar" /> < div class = "container" > < h1 >< span th:text = "${message.title}" >The message title</ span ></ h1 > < span th:text = "${message.text}" >The message text</ span > </ div > </ body > </ html > |
1
2
3
4
5
6
|
insert into user (id,username, password ,role) values (0, 'max' , 'password' , 'ROLE_USER' ); insert into user (id,username, password ,role) values (1, 'tom' , 'password' , 'ROLE_USER' ); insert into user (id,username, password ,role) values (2, 'admin' , 'password' , 'ROLE_ADMIN' ); insert into message(id,user_id,title,text) values (100,0, 'Message for Max' , 'This message is for Max. Under /message/100 only Max or an admin should see this message. Under /privateMessage/100 only Max should see this message.' ); insert into message(id,user_id,title,text) values (110,1, 'Message for Tom' , 'This message is for Tom. Under /message/110 only Tom or an admin should see this message. Under /privateMessage/110 only Tom should see this message.' ); |
Spring Security intern
Stellen wir uns Spring Security als Blackbox vor, durch die ein Request durch muss, bevor er den Controller erreicht:

Wenn wir uns unseren Projektstand als wir Thymleaf eingeführt haben anschauen, dann besteht diese Blackbox aus 12 Filtern. Ich möchte Nachfolgend nur auf 4 davon genauer eingehen und werde die anderen nur kurz beschreiben.
WebAsyncManagerIntegrationFilter (1. Filter)
Dieser Filter kann einen `SecurityContextCallableProcessingInterceptor` registrieren, der wiederum den `SecurityContextHolder` befüllt (`preProcess`) und wieder leert (`postProcess`). Der `SecurityContextHolder` hält dabei unser `Authentication` Objekt. Wenn der Aufruf von `SecurityContextHolder.getContext().getAuthentication()` einen Rückgabewert ungleich `null` hat, dann wissen wir, dass die Authentifizierung bereits stattgefunden hat. In unserem Beispiel ist kein `SecurityContextCallableProcessingInterceptor` registriert, wodurch der Filter nichts macht.
SecurityContextPersistenceFilter (2. Filter)
Befüllt den `SecurityContextHolder` mit dem Wert aus der Session.
HeaderWriterFilter (3. Filter)
Schreibt Header wie zum Beispiel X-Frame-Options in die Response.
CsrfFilter (4. Filter)
Kümmert sich um den Csrf-Token-Schutzmechanismus. Interessant ist an dieser Stelle, dass Spring Security für einen Endpunkt für den CSRF deaktiviert wurde, eine extra Filterchain mit fehlendem `CsrfFilter` anlegt. Diese besteht dann entsprechend nur aus 11 Filtern.
LogoutFilter (5. Filter)
Führt den Logout aus.
RequestCacheAwareFilter (7. Filter)
Ersetzt den Request, falls ein identischer im Cache vorhanden ist.
SecurityContextHolderAwareRequestFilter (8. Filter)
Dieser Filter wrapped das Request Objekt und enthält zusätzliche Security Informationen wie zum Beispiel das `rolePrefix`.
SessionManagementFilter (10. Filter)
Stellt eine Authentifizierung während Requests fest und führt eine SessionAuthenticationStrategy aus. Dies kann beispielsweise eine Prüfung auf Mehrfachlogin sein.
FilterSecurityInterceptor (12. Filter)
Dies ist der erste Filter auf den ich genauer eingehen möchte. Dieser Filter sorgt für einen Aufruf von `AffirmativeBased.decide()`. Im Erfolgsfall wird uns der Zugriff auf die Resource gestattet und der Request gelangt an den Controller. Andernfalls wird eine `AccessDeniedException` geworfen. Dies ist beispielsweise dann der Fall, wenn wir als anonymer User auf eine geschützte Resource zugreifen.
ExceptionTranslationFilter (11. Filter)
Dieser Filter ruft den `FilterSecurityInterceptor` in einem try-Block auf. Falls dieser eine `AccessDeniedException` geworfen hat, so wird nun ein Redirect auf die Loginseite ausgelöst.
AnonymousAuthenticationFilter (9. Filter)
Dies ist der letzte Filter, der sich um die Authentifizierung kümmert. Falls der `SecurityContextHolder` noch leer ist, so befüllt er ihn mit einem `AnonymousAuthenticationToken`, was uns als anonymen User darstellt.
UsernamePasswordAuthenticationFilter (6. Filter)
Dieser Filter prüft zunächst, ob es sich um einen Login Request handelt. Ist dies nicht der Fall, so wird mit dem nächsten Filter weiter gemacht. Andernfalls wird der Login durchgeführt. Ist dieser nicht erfolgreich, so wird ein Redirect zur Loginseite mit einer Fehlermeldung ausgelöst. Ist er jedoch erfolgreich, dann wird der `SecurityContextHolder` mit einem `UsernamePasswordAuthenticationToken` befüllt und ein Redirect zu der Seite, die vor dem Login versucht wurde aufzurufen, durchgeführt.
Autor
Jürgen Fast arbeitet seit 2014 bei der Micromata GmbH als Softwareentwickler in verschiedenen Projekten der Logistik- und Automobilbranche. Sein Schwerpunkt liegt in der Entwicklung von javabasierten Webapplikationen.
package de.micromata.spring.security.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}