Spring security u spring bootu

Izvor: SIS Wiki
Skoči na: orijentacija, traži

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.


Defaultni username i password za spring security.png


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.


Tablice u bazi.png


user

U "user" tablici postoje već 2 zapisa: user i admin. Lozinke su kripitrane pomoću Springovog bCryptPasswordEncodera.

User tablica.png


role

U "role" tablici postoje također 2 zapisa, tj. dvije uloge: admin i user.


Role tablica.png


user_roles

U "user_roles" tablici definirano je da admin ima uloge admin i korisnik a korisnik samo ulogu korisnik.


User roles tablica.png


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:


Pocetna stranica spring.png


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".


Login spring.png

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.


403 spring.png


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


Spring hello jsp.png


Pošto admin ima ovlasti da pristupi putanji "/hello" prikazuje se "hello.jsp" kako je i definirano u controlleru.

Osobni alati
Imenski prostori
Inačice
Radnje
Orijentacija
Traka s alatima