Spring security u spring bootu
Marijan Hranj https://github.com/marhranj/sisproject
Što je Spring security
Spring security predstavlja zaseban Java framework koji služi osiguravanje aplikacije. Mogućnosti koje pruža "Spring security" su prvenstveno dodavanje autentifikacije te autorizacije vašoj aplikaciji, no i dosta drugih sigurnosnih značajki. Spring security kao takav kakvim ga danas poznajemo je prvi puta pušten u korištenje 2008. godine. Postojale su i ranije verzije koje su izlazile, no ne pod nazivom Spring security. Prva verzija izašla je 2004. godine te je imala Apache licencu.
Korištenje Spring security-a
Prvi korak je dodavanje Spring security-a u aplikaciju odnosno projekt. Na službenim Spring stranicama preporuča se korištenje Maven-a kao build tool-a u aplikacijama. Za dodavanje Spring security-a pomoću mavena u sekciju <dependencies></dependencies> dodaje se sljedeći isječak.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
Samim dodavanjem Spring security-a u projekt, automatski se dodaju neke sigurnosne značajke same po sebi, npr. želimo li pristupiti sadržaju automatski se traže defaultni Spring security username i password za pristup sadržaju. Defaultni username je "user", dok se password svakim pokretanjem aplikacije ponovno izgenerira te ispisuje u konzoli prilikom builda aplikacije.
Pristupamo spring boot aplikaciji na defaultnom portu 8080 te vidimo kako Spring security obavlja svoj dio te traži username i password.
U sljedećim sekcijama modificirat ću aplikaciju kako bi se prikazala prava moć spring security-a. Aplikacija će sadržavati autentifikaciju te autorizaciju pomoću Spring security-a.
Controlleri u aplikaciji
package com.sis.project.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class WebController { @GetMapping("/") public String indexPage(Model model) { return "home"; } @GetMapping("/hello") public String helloPage(Model model) { return "hello"; } @GetMapping("/login") public String loginPage(Model model) { return "login"; } @GetMapping("/403") public String forbiddenPage(Model model) { return "403"; } }
Controlleri služe da se za određenu putanju koju se zahtjeva prikaže određeni html predložak, odnosno .jsp u ovom slučaju. Kada se ode na putanju "/" prikazati će se home.jsp, na putanju "/hello" prikazati će se hello.jsp, na putanju "/login" prikazati će se "login.jsp", na putanju "403" prikazati će se "403.jsp"
Templates
Templates predstavljaju obični markup (.jsp stranice).
U ovoj aplikaciji koristiti će se 4 templatea:
-Login - login stranica -Hello - stranica kojoj će moći pristupiti samo korisnici s određenim ovlastima -Home - stranica na koju se dolazi prilikom logina -403 - stranica koja će se prikazivati u slučaju da neovlašteni korisnik želi pristupiti sadržaju
Login
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <!DOCTYPE html> <html> <head> <link href="<c:url value="css/app.css" />" rel="stylesheet" type="text/css"> <title>SIS spring security</title> </head> <body class="security-app"> <div class="details"> <h2>SIS spring security</h2> </div> <form action="/login" method="post"> <div class="lc-block"> <div> <input type="text" class="style-4" name="username" placeholder="Korisnicko ime" /> </div> <div> <input type="password" class="style-4" name="password" placeholder="Lozinka" /> </div> <div> <input type="submit" value="Login" class="button red small" /> </div> <c:if test="${param.error ne null}"> <div class="alert-danger">Netocno korisnicko ime ili lozinka.</div> </c:if> <c:if test="${param.logout ne null}"> <div class="alert-normal">Uspjesno ste se odjavili.</div> </c:if> </div> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> </form> </body> </html>
Hello
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@taglib uri="http://www.springframework.org/tags/form" prefix="form"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <!DOCTYPE html> <html> <head> <link href="<c:url value="css/app.css" />" rel="stylesheet" type="text/css"> <title>SIS spring security</title> </head> <body class="security-app"> <div class="details"> <h2>SIS spring security</h2> </div> <div class="lc-block"> <h1> Pozdrav <b><c:out value="${pageContext.request.remoteUser}"></c:out></b> </h1> <form action="/logout" method="post"> <input type="submit" class="button red big" value="Odjava" /> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> </form> </div> </body> </html>
Home
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <!DOCTYPE html> <html> <head> <link href="<spring:url value="css/app.css" />" rel="stylesheet" type="text/css"> <title>SIS spring security</title> </head> <body class="security-app"> <div class="details"> <h2>SIS spring security</h2> </div> <div class="lc-block"> <h1>Welcome!</h1> <div class="alert-normal"> Pritisnite <a href="<spring:url value='/hello' />">ovdje</a> da biste pristupili sadržaju </div> </div> </body> </html>
403
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <link href="css/app.css"rel="stylesheet" type="text/css"></link> <title>SIS spring security</title> </head> <body class="security-app"> <div class="details"> <h2>SIS spring security</h2> </div> <div class="lc-block"> <div class="alert-danger"> <h3>Nemate prava za pristup ovoj stranici!</h3> </div> <form action="/logout" method="post"> <input type="submit" class="button red big" value="Odjava" /> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> </form> </div> </body> </html>
Baza podataka
U bazi podataka koj se koristi za ovu aplikaciju postoje tablice "user" koja prestavlja korisnika, "role" koja predstavlja korisnikove uloge te "user_roles" koja povezuje te dvije tablice.
user
U "user" tablici postoje već 2 zapisa: user i admin. Lozinke su kripitrane pomoću Springovog bCryptPasswordEncodera.
role
U "role" tablici postoje također 2 zapisa, tj. dvije uloge: admin i user.
user_roles
U "user_roles" tablici definirano je da admin ima uloge admin i korisnik a korisnik samo ulogu korisnik.
Modeli
Modeli predstavljaju entitete koje se preslikavaju na entitete, odnosno tablice u bazi podataka (princip ORM). U projektu nalaze se dvije klase modela. Model User i model Role.
User
package com.sis.project.model; import com.sis.project.utils.GenderEnum; import org.hibernate.validator.constraints.NotEmpty; import org.springframework.format.annotation.DateTimeFormat; import javax.persistence.*; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.ArrayList; import java.util.Date; import java.util.List; import static javax.persistence.GenerationType.IDENTITY; @Entity @Table(name = "user", catalog = "employees") public class User { private Integer userId; @NotEmpty private String firstName; @NotEmpty private String lastName; @NotEmpty @Size(min = 11, max = 11) private String oib; @NotNull private GenderEnum gender; @DateTimeFormat(pattern = "dd.MM.yyyy") @NotNull private Date dateOfBirth; @NotEmpty private String birthPlace; @NotEmpty private String citizenship; private String username; private String password; @NotEmpty private String email; private byte[] avatar; private Byte active; private List<Role> roles = new ArrayList<>(); public User() { } @Column(name = "active") public Byte getActive() { return active; } @Column(name = "avatar") public byte[] getAvatar() { return avatar; } @Column(name = "birth_place", length = 45) public String getBirthPlace() { return birthPlace; } @Column(name = "citizenship", length = 45) public String getCitizenship() { return citizenship; } @Temporal(TemporalType.DATE) @Column(name = "date_of_birth", length = 10) public Date getDateOfBirth() { return dateOfBirth; } @Column(name = "email", length = 45) public String getEmail() { return email; } @Column(name = "first_name", length = 45) public String getFirstName() { return firstName; } @Column(name = "gender") public GenderEnum getGender() { return gender; } @Column(name = "last_name", length = 45) public String getLastName() { return lastName; } @Column(name = "oib", length = 45) public String getOib() { return oib; } @Column(name = "password") public String getPassword() { return password; } @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "user_roles", catalog = "employees", joinColumns = { @JoinColumn(name = "user_id", nullable = false, updatable = false) }, inverseJoinColumns = { @JoinColumn(name = "role_id", nullable = false, updatable = false) }) public List<Role> getRoles() { return roles; } @Id @GeneratedValue(strategy = IDENTITY) @Column(name = "user_id", unique = true, nullable = false) public Integer getUserId() { return userId; } @Column(name = "username", length = 45) public String getUsername() { return username; } }
Role
package com.sis.project.model; import javax.persistence.*; import java.util.ArrayList; import java.util.List; import static javax.persistence.GenerationType.IDENTITY; @Entity @Table(name = "role", catalog = "employees") public class Role { private Integer roleId; private String name; private Byte active; private List<User> users = new ArrayList<>(); public Role() { } public Role(String name, Byte active, List<User> users) { this.name = name; this.active = active; this.users = users; } @Column(name = "active") public Byte getActive() { return active; } @Column(name = "name", length = 45) public String getName() { return name; } @Id @GeneratedValue(strategy = IDENTITY) @Column(name = "role_id", unique = true, nullable = false) public Integer getRoleId() { return roleId; } @ManyToMany(fetch = FetchType.EAGER, mappedBy = "roles") public List<User> getUsers() { return users; } }
Repository
Repozitorije čine sučelja koja su vezani za modele. Svaki model bi trebao imati svoj repozitorije. Repozitorij ima metode za CRUD operacije nad entitetom u bazi.
U ovoj aplikaciji postoje 2 repository sučelja, za usera i za role.
User repository
package com.sis.project.repository; import com.sis.project.model.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Integer> { User findByUsernameOrEmail(String username, String email); }
Role repository
package com.sis.project.repository; import com.sis.project.model.Role; import org.springframework.data.jpa.repository.JpaRepository; public interface RoleRepository extends JpaRepository<Role, Integer> { Role findByRoleId(Integer roleId); }
User details service
Kako bi spring security znao kako da obradi korisnika (koji može biti različit za svaki projekt) prilikom logiranja mora se implementirati UserDetailsService sučelje, točnije metoda "loadUserByUsername". To je sučelje koje dolazi od Spring security-a.
package com.sis.project.security; import com.sis.project.model.Role; import com.sis.project.model.User; import com.sis.project.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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; import java.util.HashSet; import java.util.Set; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException { final User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail); if (user == null) { return new org.springframework.security.core.userdetails.User(" ", " ", new HashSet<>()); } final Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); for (final Role r : user.getRoles()) { grantedAuthorities.add(new SimpleGrantedAuthority(r.getName().toUpperCase())); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities); } }
U ovoj metodi se pomoću korisničkog imena zadanog kao parametra metode doznaje o kojem se korisniku u bazi radi. Ukoliko korisnik u bazi ne postoji, vraća se nepostojeći korisnik. Ukoliko postoji, za korisnika se pronađu sve njegove ovlasti na temelju uloga (roles) te dodaju u set ovlasti (java.util.Set).
Metoda mora vratiti UserDetails objekt koji također dolazi od Spring Security-a.
Web security config
Ovo je najvažnija klasa za dodavanje značajki Spring security-a u ovom projektu. U toj se klasi definiraju sigurnosne značajke aplikacije.
Ime klase nije bitno, bitno je samo da ona ima anotacije @Configuration te @EnableWebSecurity te da extenda klasu "WebSecurityConfigurerAdapter" koja dolazi od Spring security-a.
package com.sis.project.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; 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; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordencoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasAuthority("ADMIN") .anyRequest().permitAll() .and() .formLogin().loginPage("/login").defaultSuccessUrl("/hello") .usernameParameter("username").passwordParameter("password") .and() .logout().logoutSuccessUrl("/login?logout") .and() .exceptionHandling().accessDeniedPage("/403") .and() .csrf(); } @Bean(name = "passwordEncoder") public PasswordEncoder passwordencoder() { return new BCryptPasswordEncoder(); } }
U klasi se referencira na ranije implementirani UserDetailsService.
U metodi configAuthentication se daje do znanja Spring securityu-a da prilikom provjeravanja autentifikacije i autorizacije korisnika koristi UserDetailsService ranije implementiran te da su lozinke u bazi kripitirane pomoću BCryptPasswordEncoder-a.
U metodi configure se definiraju sljedeće stvari:
-za pristup pathu "/hello" korisnik mora imati ovlast admin -definira se koja je login stranica ("/login) -definira se na koju će se stranici otići prilikom uspjšnog login-a -definira se da se username i password čita iz jsp-a sa inputima imena "username" i "password" (to nije trebalo definirati jer su username i password defaultno zadani, trebalo je jedino ukoliko name inputa u markupu nije username i password) -definira se path na koji će se otići prilikom odjave -definira se putanja na koju će se otići ukoliko dođe do neovlaštenog pristupa stranici
Slijede primjeri kako to sve zapravo izgleda:
Prilikom odlaska na putanju "/" prikazuje se stranica "home.jsp".
Zatim kliknemo na poveznicu koja je navedena na stranici "ovdje".
Da poveznica vodi na putanju "/hello". Klikom na poveznicu očekujemo da ćemo nam se prikazati stranica "hello.jsp".
Međutim u klasi WebSecurityConfig se definiralo da se za putanju "/hello" mora imati ovlasti admin a još niti jedan korisnik nije autentificiran te se prvo mora obaviti login te Spring security prvo preusmjerava na login stranicu.
Unosimo username: user i password: user te gledamo što će se dalje dogoditi.
Kako vidimo putanja je "hello" no umjesto "hello.jsp" Spring security preusmjerava na "403.jsp". To je onaj .jsp koji je definiran da se prikaže kada neovlašteni korisnik pristupi stranici.
Slijedi prijava sa username: admin i password: admin
Pošto admin ima ovlasti da pristupi putanji "/hello" prikazuje se "hello.jsp" kako je i definirano u controlleru.