Spring Security Nedir? Kurulumu ve Kullanımı

Java tabanlı uygulamalarda güvenlik önlemlerinde kullanılan Spring Security nedir ile başlayıp kurulumu ve kullanımı detaylı örneklerle yer alıyor.

Spring Security nedir?

Spring framework kullanılarak geliştirilen doğrulama(Authentication), yetkilendirme(Authorization), şifreleme(Password Encoder) ve CSRF gibi güvenlik önlemleri sağlayan, Spring platformunda yer alan bir projedir.

Spring framework hakkında detaylı bilgi almak için Spring framework yazıma bakmalısın.

Kullanım kolaylığı, CSRF, Session Fixation gibi bir çok güvenlik önleminin varsayılan olarak aktif gelmesi, farklı doğrulama, yetkilendirme ve şifreleme yöntemlerini desteklemesinden dolayı Spring Security de-facto standart haline gelmiştir.

Neden Spring Security?

Kullanıcılar uygulamalardan beklenen işlevin yanından kullanıcıların sahip olduğu yetkilere(yönetim, editör veya üye) göre işlem yapması istenebilir.

Uygulama bu ihtiyaç ile birlikte doğrulama, yetkilendirme ve saldırganın yetkileri elde ederek uygulama işlevini değiştirmesini önleme gibi güvenlik tebdirlerine gereksinim duyulur.

Her bir gereksinim içerisinde başka gereksinimlere ihtiyaç duyabilir.

Örneğin; Doğrulama sıradan bir dosyada yer alan kullanıcı bilgilerinin kontrolü, veritabanından kontrolü veya LDAP gibi protokollerden kontrolü olabilir.

Temel doğrulama ve yetkilendirmenin yanında saldırganlar sürekli olarak farklı saldırı yöntemleri kullanılarak güvenlik açıkları ortaya çıkarmaktadır.

Tüm bu güvenlik gereksinimleri uygulamadan beklenen işlevseliği yerine getirmek için kullanılan efor kadar efora neden olacaktır.

Spring Security doğrulama, yetkilendirme, şifreleme, güvenlik önlemlerini esnek, kolay ve sürekli olarak paketlerin güncellenmesi ile sağlar.

Spring Security kurulumu

Spring Security kullanım alanı, doğrulama ve yetkilendirme işlemine göre farklı paketlere(spring-security-core, spring-security-config, spring-security-web vb.) ayrılmıştır.

NOT: LDAP, OAuth gibi bazı paketler ek kütüphaneye ihtiyaç duyabilir.

Sıklıkla kullanıldığı ve geniş kullanım desteği sağladığı için web tabanlı uygulama paketi(spring-security-web) kullanılacaktır.

Maven içerisinde yer alan archetype özelliğini kullanarak web projesi oluşturalım.

mvn archetype:generate 
-DgroupId=com.yusufsezer 
-DartifactId=SpringSecurity 
-DarchetypeArtifactId=maven-archetype-webapp 
-DinteractiveMode=false

Maven hakkında detaylı bilgi almak için Maven yazıma bakmalısın.

Spring Security Web ve Spring Security Config paketlerini pom.xml dosyasına eklenerek kurulumu tamamlayalım.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.4.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.4.2</version>
</dependency>

Spring Security Web, JCP tarafından belirlenen Servlet(JSR 369) gibi şartnameleri kullanır.

Her bir şartnameye ait paketin tek tek eklenmesi yerine JEE 8 paketinin projeye eklenmesi karmaşıklığı azaltacaktır.

<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>8.0.1</version>
    <scope>provided</scope>
</dependency>

Spring Security için gerekli olan modüller(spring framework, spring security core vb.) maven tarafından projeye eklenecektir.

Spring Security kullanımı

Spring Security web projelerinde güvenliği sağlamak için Servlet şartnamesinde yer alan Filter özelliğini kullanır.

Filtreleri aktif etmek için XML tabanlı(web.xml) veya Java tabanlı(AbstractSecurityWebApplicationInitializer) ayarlar kullanılabilir.

Spring Security filtrelerinin çalışması için Spring ve Servlet arasında köprü-bağlantı görevi gören DelegatingFilterProxy filtresi kullanılır.

<?xml version="1.0" encoding="UTF-8"?>
<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

ContextLoaderListener, DelegatingFilterProxy filtresinin Spring ile bağlantı kurarken ihtiyaç duyduğu WebApplicationContext(RootContext) oluşturmayı sağlar.

ContextLoaderListener varsayılan olarak XmlWebApplicationContext sınıfını kullanarak ayarları /WEB-INF/applicationContext.xml dosyasından alır.

Spring Framework ve Spring Security ayarlarını applicationContext.xml dosyasına aşağıdaki gibi yapmak yeterli olacaktır.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:sec="http://www.springframework.org/schema/security"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/security 
                            http://www.springframework.org/schema/security/spring-security.xsd">

    <sec:http />

    <sec:user-service>
        <sec:user name="yusuf" password="{noop}123456" authorities="ROLE_ADMIN" />
    </sec:user-service>

</beans>

Okunabilirliğini arttırmak için xmlns değerine security tanımı yapılabilir.

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:beans="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd
                            http://www.springframework.org/schema/security 
                            http://www.springframework.org/schema/security/spring-security.xsd">

    <http />

    <user-service>
        <user name="yusuf" password="{noop}123456" authorities="ROLE_ADMIN" />
    </user-service>

</beans:beans>

XML kullanımı yazım hatalarına açık olduğu, derleme zamanında hataların ortaya çıkmadığından dolayı Servlet 3.0 ile birlikte gelen Java tabanlı ayarlar sıklıkla kullanılmaktadır.

Java tabanlı ayarlarda Spring MVC veya herhangi bir kütüphanesiye ihtiyaç duymadan AbstractSecurityWebApplicationInitializer sınıfının kullanılması yeterli olacaktır.

public class SecurityInitializer
        extends AbstractSecurityWebApplicationInitializer {

    public SecurityInitializer() {
        super(SecurityConfig.class);
    }

}

Doğrulama, yetkilendirme, şifreleme ve diğer ayarların yapılması için WebSecurityConfigurerAdapter sınıfı kalıtım alınarak düzenlenir.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {}

Spring Security kullanıcı giriş işlemlerinin yanında farklı güvenlik yöntemlerini sağlamak için @EnableWebSecurity ifadesini kullanır.

İfade içsel olarak Spring Security paketinde yer alan çeşitli sınıfları-filtreleri @Import ifadesi ile ekler.

Annotation hakkında detaylı bilgi almak için Java Annotations yazıma bakmalısın.

Proje WebSecurityConfigurerAdapter sınıfında belirlenen varsayılan ayarlara göre çalışacak ve doğrulama sağlayıcısı(AuthenticationProvider) verilmediğinden giriş işleminde hata mesajı verecektir.

XML ayarları(web.xml) kullanılmadan oluşturulan war dosyalarındaki uyarıyı gidermek için pom.xml dosyasına aşağıdaki ayarın eklenmesi gerekebilir.

<properties>
    <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>

WebSecurityConfigurerAdapter sınıfında yer alan configure metotları override-ezilerek Spring Security özelleştirilir.

Spring Security nasıl çalışır

Spring Security Web, Servlet şartnamesinde yer alan Filter özelliğini kullanarak güvenlik işlemlerini yapar.

Her Filter sınıfının aktif edilmesi için ayrıca Servlet Container(tomcat, jetty vb.) ile kayıt edilmesi gerekir.

Filtrelerin kayıt edilmesinin yanında Spring Security filtrelerinin çalışması için Spring framework IoC Container ile bağlantı kurulması gerekir.

Spring Security bu işlemi yapmak için FilterChainProxy sınıfını geliştirmiş daha sonra bu sınıf spring-web modülüne DelegatingFilterProxy olarak taşınmıştır.

Sıradan bir Servlet Filter olan DelegatingFilterProxy filtre işleminin yapıldığı adımda(doFilter) Servlet Context alanında yer alan WebApplicationContext sınıfını(ContextLoaderListener ile oluşturulan) kullanarak Spring Security filtrelerini Spring framework IoC Container ile oluşturur.

Filtreler Spring framework IoC Container ile oluşturulduğundan dolayı filtre içerisinde Spring framework tarafından sağlanan özellikleri kullanmayı sağlar.

Bu sayede her bir Servlet filtresinin ayrı ayrı eklenmesine gerek olmadan DelegatingFilterProxy üzerinden dinamik olarak oluşturulması sağlanır.

NOT: DelegatingFilterProxy sınıfı Delegation yöntemini kullanarak filtreleri FilterChainProxy üzerinden çalıştırır.

FilterChainProxy oluşturulan filtreleri(CsrfFilter, LogoutFilter vb.) sırasıyla çalıştırararak Spring Security işlemini yapmış oluyor.

NOT2: Filtreler Spring tarafından oluşturulacağı(nesne haline geleceği) için yapılan ayarlara göre filtre sayısı dinamik olarak değişecektir.

Authentication – Doğrulama

Doğrulama işlemlerinde in-Memory, JDBC, UserDetailsService, LDAP ve AuthenticationProvider yöntemleri yer alır.

in-Memory

Deneme/Test amaçlı olarak kullanılmaktadır.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.inMemoryAuthentication()
                .withUser("yusuf")
                .password("{noop}123456")
                .roles("ADMIN");
    }

}

NOT: Kullanıcı bilgileri uygulama çalıştığı sürece HashMap veri türünde saklanır.

JDBC

İlişkisel veritabanında yer alan bilgiler üzerinden doğrulama yapmak için JDBC doğrulama yöntemi kullanılır.

JDBC doğrulama yöntemi spring-jdbc modülü ve veritabanına ait sürücü/arabirim ile çalışır.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.11.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
</dependency>

NOT: Spring Security ve Spring Framework modül sürümlerinin aynı olmasına dikkat edilmelidir.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.jdbcAuthentication()
                .dataSource(dataSource())
                .withDefaultSchema()
                .withUser("yusuf")
                .password("{noop}123456")
                .roles("ADMIN");
    }

}

JDBC ayarları JdbcUserDetailsManagerConfigurer sınıfı kullanılarak yapılandırılır.

Sınıf içerisindeki withDefaultSchema metodu varsayılan JDBC tablosunu oluşturur.

Varsayılan JDBC tablosu aşağıdaki dosyada yer alır ve sadece gömülü H2 veritabanı ile çalışır.

org/springframework/security/core/userdetails/jdbc/users.ddl

Aşağıdaki SQL komutları kullanılan veritabanı yönetim sistemine göre düzenlenerek varsayılan tablo ve sütunlar withDefaultSchema metoduna ihtiyaç olmadan kullanılabilir.

CREATE TABLE users (
  username VARCHAR(50) NOT NULL PRIMARY KEY,
  PASSWORD VARCHAR(500) NOT NULL,
  enabled boolean NOT NULL
);
CREATE TABLE authorities (
  username VARCHAR(50) NOT NULL,
  authority VARCHAR(50) NOT NULL,
  CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username),
  CONSTRAINT ix_auth_username UNIQUE KEY(username, authority)
);

NOT: Veritabanı yönetim sistemine göre veri kaynağı (dataSource metodu) düzenlenmelidir.

in-Memory ve JDBC yöntemleri UserDetailsManager arayüzünün uygulamalarıdır.

UserDetailsManager arayüzü UserDetailsService arayüzünü kalıtarak kullanıcı işlemleri (createUser, updateUser, deleteUser vb.) yapmayı sağlar.

NOT: JDBC yöntemi UserDetailsService arayüzünü JdbcDaoImpl sınıfı ile sağlar.

AuthenticationManagerBuilder sınıfı doğrudan bu sınıflarda yer alan metotların kullanımını sağlamaz.

Metotları kullanabilmek için ayrı bir bean tanımı yapılabilir.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public UserDetailsManager userDetailsManager() {
        UserDetailsManager myDetailsManager = new InMemoryUserDetailsManager();

        UserDetails yusuf = User.withUsername("yusuf")
                .password("{noop}123456")
                .roles("ADMIN")
                .build();

        UserDetails sefa = User.withUsername("sefa")
                .password("{noop}123456")
                .roles("MEMBER")
                .build();

        myDetailsManager.createUser(yusuf);
        myDetailsManager.createUser(sefa);
        //myDetailsManager.updateUser(sefa);
        //myDetailsManager.deleteUser(sefa);

        // Giriş yapmış kullanıcı bilgileri ile çalışır.
        //myDetailsManager.changePassword(oldPassword, newPassword);

        String username = "yusuf";
        boolean userExists = myDetailsManager.userExists(username);
        System.out.println(username + " yer al" + (userExists ? "ıyor" : "mıyor"));

        return myDetailsManager;
    }

}

NOT: Metotların kullanımını göstermek amacıyla bean tanımı içerisinde metotlar kullanılmıştır.

UserDetailsManager arayüzünü uygulayan InMemoryUserDetailsManager Spring IoC bean olarak tanımlandığından Spring Framework otomatik bağlama özelliği ile kullanılabilir.

@Service
public class UserService {

    @Autowired
    private UserDetailsManager userDetailsManager;

    public void kayit(UserDetails userDetails) {
        userDetailsManager.createUser(userDetails);
    }

    public void sil(String username) {
        userDetailsManager.deleteUser(username);
    }

    // diğer işlemler

}

Kullanıcı işlemleri ayrıca geliştirme-kod yazımı yapılmadan, UserDetailsManager metotları üzerinden yapılmış olur.

JDBC yöntemi GroupManager arayüzünün uygulayarak grup işlemleri yapmayı sağlar.

Aşağıdaki SQL komutları kullanılan veritabanı yönetim sistemine göre düzenlenerek grup işlemleri yapılır.

CREATE TABLE groups (
	id BIGINT PRIMARY KEY AUTO_INCREMENT,
	group_name VARCHAR(50) NOT NULL
);
CREATE TABLE group_authorities (
	authority VARCHAR(50) NOT NULL,
	group_id BIGINT NOT NULL,
	CONSTRAINT fk_group_authorities_group FOREIGN KEY(group_id) REFERENCES groups(id)
);
CREATE TABLE group_members (
	id BIGINT PRIMARY KEY AUTO_INCREMENT,
	username VARCHAR(50) NOT NULL,
	group_id BIGINT NOT NULL,
	CONSTRAINT fk_group_members_group FOREIGN KEY(group_id) REFERENCES groups(id)
);

Grup işlemleri (JdbcUserDetailsManager) bean tanımı yapılarak kullanılır.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public DataSource dataSource() {
        String url = "jdbc:mysql://localhost:3306/demo";
        String username = "root";
        String password = "";
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUrl(url);
        driverManagerDataSource.setUsername(username);
        driverManagerDataSource.setPassword(password);
        return driverManagerDataSource;
    }

    @Bean
    public JdbcUserDetailsManager jdbcUserDetailsService()
            throws Exception {

        UserDetails yusuf = User.withDefaultPasswordEncoder()
                .username("yusuf")
                .password("123456")
                .roles("ADMIN")
                .build();

        UserDetails sezer = User.withDefaultPasswordEncoder()
                .username("sezer")
                .password("123456")
                .roles("Moderator")
                .build();

        UserDetails sefa = User.withDefaultPasswordEncoder()
                .username("sefa")
                .password("123456")
                .roles("Member")
                .build();

        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource());

        // kullanıcı işlemleri
        jdbcUserDetailsManager.createUser(yusuf);
        jdbcUserDetailsManager.createUser(sefa);
        jdbcUserDetailsManager.createUser(sezer);

        // grup işlemleri
        String groupName = "Yönetim";
        jdbcUserDetailsManager.createGroup(groupName,
                Arrays.asList(
                        new SimpleGrantedAuthority("ADMIN"),
                        new SimpleGrantedAuthority("Member"))
        );

        String groupName2 = "Üyeler";
        jdbcUserDetailsManager.createGroup(groupName2,
                Arrays.asList(
                        new SimpleGrantedAuthority("Diğer")
                )
        );

        jdbcUserDetailsManager.addUserToGroup("yusuf", groupName);
        jdbcUserDetailsManager.addUserToGroup("sezer", groupName);
        jdbcUserDetailsManager.addUserToGroup("sefa", groupName2);
        jdbcUserDetailsManager.addUserToGroup("yok", groupName2);
        jdbcUserDetailsManager.addUserToGroup("yok1", groupName);

        jdbcUserDetailsManager.renameGroup(groupName, "Yöneticiler");

        jdbcUserDetailsManager.removeUserFromGroup("sefa", groupName2);
        jdbcUserDetailsManager.deleteGroup(groupName2);

        System.out.println("Gruplar");
        jdbcUserDetailsManager.findAllGroups().forEach(System.out::println);

        System.out.println(groupName + " grubundaki kullanıcılar");
        jdbcUserDetailsManager.findUsersInGroup(groupName).forEach(System.out::println);

        System.out.println(groupName + " grubuna ait roller");
        jdbcUserDetailsManager.findGroupAuthorities(groupName).forEach(System.out::println);

        UserDetails loadUserByUsername = jdbcUserDetailsManager.loadUserByUsername("yusuf");
        System.out.println(loadUserByUsername);

        return jdbcUserDetailsManager;
    }

}

JdbcUserDetailsManager sınıfı UserDetailsService, UserDetailsManager ve GroupManager arayüzünü uyguladığı için bu arayüze ait metotlara yukarıdaki gibi JdbcUserDetailsManager sınıfına ait bean tanımı üzerinden erişilebilir.

@Service
public class GroupService {

    @Autowired
    private GroupManager groupManager;

    public void ekle(String groupName, String... authorities) {
        groupManager.createGroup(
                groupName,
                AuthorityUtils.createAuthorityList(authorities)
        );
    }

    public void sil(String groupName) {
        groupManager.deleteGroup(groupName);
    }

    public List<String> gruplar() {
        return groupManager.findAllGroups();
    }

    // diğer işlemler

}

NOT: Bean geri dönüş değeri UserDetailsService veya UserDetailsManager kullanıldığında sadece belirtilen arayüze ait otomatik bağlama özelliği doğrudan kullanılır.

JDBC doğrulama yöntemi varsayılan tablo yapısının yanında özel tablo yapısını kullanmayı destekler.

CREATE TABLE kullanicilar (
  id INT NOT NULL AUTO_INCREMENT,
  email VARCHAR(100) NOT NULL,
  sifre VARCHAR(100) NOT NULL
);
CREATE TABLE yetkiler (
  id INT NOT NULL AUTO_INCREMENT,
  email VARCHAR(100) NOT NULL,
  rol VARCHAR(100) NOT NULL
);

NOT: Özel tablo yapısı uygulamaya göre değişiklik gösterebilir.

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
            .dataSource(this.dataSource())
            .usersByUsernameQuery("SELECT email, sifre, true as enabled FROM kullanicilar WHERE email = ?")
            .authoritiesByUsernameQuery("SELECT email, rol FROM yetkiler WHERE email = ?")
            .passwordEncoder(NoOpPasswordEncoder.getInstance());
}

JDBC kullanıcı bilgilerini almak için usersByUsernameQuery metodu ile belirtilen sorguyu, kullanıcı yetkilerini almak için ise authoritiesByUsernameQuery ile belirtilen sorguyu kullanacaktır.

Kullanıcı şifresi ile veritabanında yer alan şifrenin karşılaştırılması için passwordEncoder metodu ile PasswordEncoder arayüzünü uygulayan şifreleme yöntemi kullanılır.

Örnekte herhangi bir şifreleme kullanılmadığı için NoOpPasswordEncoder sınıfı kullanılmıştır.

Şifreleme yöntemi MD5, SHA1 gibi hash tabanlı şifrelem yöntemi kullanıyorsa Spring Security paketinde yer alan sınıflar kullanılabilir.

  • MD4 – org.springframework.security.crypto.password.Md4PasswordEncoder()
  • MD5 – org.springframework.security.crypto.password.MessageDigestPasswordEncoder(“MD5”)
  • SHA-1 – MessageDigestPasswordEncoder(“SHA-1”)
  • SHA-256 – MessageDigestPasswordEncoder(“SHA-256”)
  • vb.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.jdbcAuthentication()
            .dataSource(this.dataSource())
            .usersByUsernameQuery("SELECT email, sifre, true as durum FROM kullanicilar WHERE email = ?")
            .authoritiesByUsernameQuery("SELECT email, rol FROM yetkiler WHERE email = ?")
            .passwordEncoder(new MessageDigestPasswordEncoder("MD5"));
}

NOT: Hash şifreleme yöntemi yerine hash ile birlikte salt tabanlı şifreleme sağlayan bcrypt, argon2, pbkdf2, scrypt yöntemlerinin kullanımı faydalı olacaktır.

JdbcUserDetailsManager sınıfında yer alan metotlar kullanılarak kullanıcı ve grup işlemlerinde kullanılan SQL komutları düzenlenebilir.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // diğer tanımlar

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public JdbcUserDetailsManager jdbcUserDetailsManager() {
        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource());
        jdbcUserDetailsManager.setUsersByUsernameQuery("SELECT email, sifre, true as durum FROM kullanicilar WHERE email = ?");
        jdbcUserDetailsManager.setAuthoritiesByUsernameQuery("SELECT email, rol FROM yetkiler WHERE email = ?");
        //jdbcUserDetailsManager.setCreateUserSql(createUserSql);
        //jdbcUserDetailsManager.setUpdateUserSql(updateUserSql);
        //jdbcUserDetailsManager.setDeleteUserSql(deleteUserSql);
        //jdbcUserDetailsManager.setInsertGroupSql(insertGroupSql);
        //jdbcUserDetailsManager.setRenameGroupSql(renameGroupSql);
        //jdbcUserDetailsManager.setDeleteGroupSql(deleteGroupSql);
        return jdbcUserDetailsManager;
    }

}

NOT: Spring Security tarafından sağlanan varsayılan tablo yapısının kullanımı önerilir.

UserDetailsService

Kullanıcı ve şifre bilgilerini farklı veri kaynağından almak için UserDetailsService arayüzü kullanılır.

Arayüz kullanıcı adını loadUserByUsername metoduna parametre olarak göndererek UserDetails arayüzünü uygulayan bir sınıf döndürmesini bekler.

UserDetailsService arayüzünün kullanarak metin belgesinde yer alan kullanıcı bilgilerini doğrulayalım.

Aşağıdaki bilgileri resources dizinine users.txt olarak kayıt edelim.

yusuf-123
sezer-456

Arayüzü uygulayan sınıfı hazırlayalım.

public class TextUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        Map<String, String> users = getUsers("users.txt", "-");

        if (!users.containsKey(username)) {
            throw new UsernameNotFoundException(username);
        }

        return new User(username, users.get(username), AuthorityUtils.NO_AUTHORITIES);
    }

    public Map<String, String> getUsers(String fileName, String delim) {
        List<String> userList = readResourceFileAsList(fileName);
        return userList.stream()
                .map(user -> user.split(delim))
                .collect(Collectors.toMap(
                        username -> username[0],
                        password -> password[1],
                        (user, password) -> user));
    }

    List<String> readResourceFileAsList(String fileName) {
        URL fileUrl = getClass().getClassLoader().getResource(fileName);
        File textFile = new File(fileUrl.getFile());
        try {
            return Files.readAllLines(textFile.toPath());
        } catch (IOException e) {
            return Collections.emptyList();
        }
    }

}

Sınıf loadUserByUsername metoduna gönderilen kullanıcı bilgisini users.txt dosyasından alarak UserDetails arayüzünü uygulayan User sınıfını döndürecektir.

Sınıfın örneği AuthenticationManagerBuilder#userDetailsService metoduna parametre olarak gönderilip kullanılır.

@Override
protected void configure(AuthenticationManagerBuilder auth)
        throws Exception {
    auth.userDetailsService(new TextUserDetailsService())
            .passwordEncoder(NoOpPasswordEncoder.getInstance());
}

NOT: Şifreleme yöntemi kullanılmadığından NoOpPasswordEncoder kullanılmıştır.

Kullanıcı adı ve şifre bilgilerinin farklı veri kaynağına saklandığı durumlarda UserDetailsService sıklıkla(özellikle Spring Data ile birlikte) kullanılmaktadır.

Spring Data hakkında detaylı bilgi almak için Spring Data yazıma bakmalısın.

NOT: in-Memory ve JDBC yöntemleri UserDetailsService arayüzünün uygulamalarıdır.

NOT2: UserDetailsService arayüzü DaoAuthenticationProvider doğrulama sağlayıcısı tarafından yönetilir/kullanılır.

AuthenticationProvider

Spring Security erişim bilgilerini doğrulamak için kullanıcı adı ve şifre, üst bilgi (header), merkezi doğrulama gibi farklı doğrulama yöntemlerini destekler.

Doğrulama yöneticisi ilgili filtre adımında, doğrulama yönteminin çalışması için gereken parametreler sağlandığında doğrulama işlemini yapar.

Doğrulama işlemi AuthenticationManager arayüzünü uygulayan ProviderManager sınıfı tarafından yönetilir.

ProviderManager doğrulama işlemlerini yapmak için doğrulama sağlayıcılarını (AuthenticationProvider) sırasıyla çalıştırarak doğrulamayı yapar.

Kullanıcı adı ve şifre doğrulama için DaoAuthenticationProvider, LDAP servisi için LdapAuthenticationProvider, SAML yöntemine göre doğrulama için OpenSamlAuthenticationProvider gibi sınıfları/doğrulama sağlayıcıları kullanır.

Doğrulama sağlayıcısı doğrulama işlemini başarıyla tamamladığında erişim bilgilerinin yer aldığı Authentication-Principal arayüzünü uygulayan bir sınıf oluşturur.

Erişim bilgilerine Spring Security ile erişimek için SecurityContextHolder#getContext, SecurityContext#getAuthentication adımları kullanılır.

SecurityContextHolder.getContext().getAuthentication()

NOT: Erişim bilgilerine farklı yöntemlerle(Spring Security, Servlet-Principal) erişilebilir.

Kullanılan doğrulama yöntemine göre doğrulama sağlayıcısının çalışması farklılık gösterir.

Örneğin; Bir önceki başlıkta yer alan UserDetailsService arayüzü kullanıcı bilgilerini veri kaynağından alarak veri kaynağındaki şifreyi girilen şifre ile PasswordEncoder arayüzü üzerinden karşılaştırarak doğrulama yapar.

LDAP gibi kullanıcı bilgilerine erişimin olmadığı durumlarda UserDetailsService veya DaoAuthenticationProvider bilgilere erişemediği için doğrulama işlemini yapamayacaktır.

Kullanıcı bilgilerine erişimin olmadığı doğrulama yönteminde AuthenticationProvider arayüzü kullanılabilir.

public class MyAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        String username = authentication.getName();
        Object credentials = authentication.getCredentials();
        if (!(credentials instanceof String)) {
            return null;
        }
        String password = credentials.toString();

        boolean result = checkUsernamePassword(username, password);

        if (!result) {
            throw new BadCredentialsException("Doğrulama başarısız");
        }

        return new UsernamePasswordAuthenticationToken(
                username,
                password,
                AuthorityUtils.NO_AUTHORITIES);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }

    protected boolean checkUsernamePassword(String username, String password) {
        // bir web isteği sonucu bilgilerin kontrol edilmesi olabilir
        return username.equals(password);
    }

}

AuthenticationException sınıfını kalıtım alan BadCredentialsException, DisabledException gibi sınıflar kullanılarak doğrulama hatası ile ilgili detaylı bilgi verilebilir.

NOT: Saldırganların güvenliği aşmasının önüne geçmek için detaylı bilgi verilmesi tavsiye edilmez.

Sınıf örneği AuthenticationManagerBuilder#authenticationProvider metoduna parametre olarak gönderilerek

@Override
protected void configure(AuthenticationManagerBuilder auth)
        throws Exception {
    auth.authenticationProvider(new MyAuthenticationProvider());
}

veya bean tanımlaması yapılarak kullanılır.

@Bean
public AuthenticationProvider authenticationProvider() {
    return new MyAuthenticationProvider();
}

AuthenticationProvider#authenticate metoduna parametre olarak gönderilen sınıf UsernamePasswordAuthenticationFilter filtresi tarafından sağlanır.

Filtreler

UsernamePasswordAuthenticationFilter filtresi form kullanılarak gönderilen kullanıcı adı ve şifre bilgilerinin yer aldığı UsernamePasswordAuthenticationToken sınıfını oluşturarak AuthenticationProvider#authenticate metoduna iletir.

Spring Security farklı doğrulama yöntemlerini filtreler kullanarak sağlar.

Örneğin; HTTP isteği sırasında gönderilen üst bilgi (header) içerisinde yer alan bilgiyi kullanarak doğrulama yapan Basic Authentication doğrulama yönteminin çalışması için gerekli olan bilgiler BasicAuthenticationFilter filtresi tarafından sağlanır.

Filtre ve doğrulama sağlayıcısı özelliği kullanılarak JWT, OAuth veya özel doğrulama yöntemi geliştirilebilir.

LDAP

Kurumsal veya belirli bir sayıdan fazla kişinin çalıştığı yerlerde doğrulama, yetkilendirme gibi bilgiler LDAP protokolü kullanılarak saklanır.

Spring Security LDAP protokolünü AuthenticationManagerBuilder#ldapAuthentication metodu LdapAuthenticationProviderConfigurer sınıfı üzerinden düzenlemeyi sağlar.

LDAP protokolünü kullanabilmek için Spring Security LDAP paketinin pom.xml eklenmesi gerekir.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
    <version>5.4.2</version>
</dependency>

LDAP protokolü esnek ve geniş yapılandırmaya sahip olduğundan ayarlar kullanılan sisteme göre farklılık gösterebilir.

Spring Security gömülü LDAP sunucusu oluşturmak için kullanılan UnboundID ve ApacheDS kütüphanelerini destekler.

Gömülü LDAP sunucusu oluşturmak için kütüphaneye ait paketin pom.xml dosyasına eklenmesi yeterli olacaktır.

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>5.1.3</version>
</dependency>

LDAP bilgileri IETF tarafından RFC 2849 ile belirtilen nitelik-attribute olarak belirli bir şema-schema kullanarak ldif dosyasında saklanır.

Aşağıdaki bilgileri resources dizinine ldif uzantılı dosya olarak kayıt edelim.

dn: ou=people,dc=yusufsezer,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=yusuf,ou=People,dc=yusufsezer, dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
uid: yusuf
userPassword: 123456
cn: Yusuf SEZER
cn: Yusuf Sefa SEZER
sn: SEZER
mail: yusufsezer@mail.com

NOT: Hızlıca ldif dosyası oluşturmak için LDIFGenerator kullanmak faydalı olacaktır.

Gömülü LDAP sunucusu için gerekli ayarları yapalım.

@Override
protected void configure(AuthenticationManagerBuilder auth)
        throws Exception {
    auth.ldapAuthentication()
            .userDnPatterns("uid={0},ou=People")
            .contextSource()
            .root("dc=yusufsezer,dc=com");
}

LdapAuthenticationProviderConfigurer sınıfı gömülü LDAP sunucusunu resources dizininde yer alan ldif uzantılı dosya bilgilerine bakarak oluşturacaktır.

Yukarıda yer alan userDnPatterns kullanıcı bilgisini aramak için kullanılacak ifadeyi, contextSource LDAP context bilgilerini(url, port, root) ayarlamayı sağlar.

NOT: Root bilgisi verilmerdiğinde varsayılan olarak dc=springframework,dc=org kullanılacaktır.

LDAP bilgisine göre LdapAuthenticationProviderConfigurer metotları kullanılarak ayarlar özelleştirilebilir.

auth.ldapAuthentication()
        .userDnPatterns("uid={0},ou=People")
        //.userSearchBase("ou=people")  // kullanıcı bölümü (ou-organizationalUnit)
        //.userSearchFilter("(uid={0})") // kullanıcı filtreleme
        //.groupSearchBase("ou=groups") // grup araması
        // .groupSearchFilter("member={0}")  // grup filtresi
        //.passwordCompare()  // şifre işlemini özelleştirme
        //.passwordAttribute("sifre")  // şifre sifre niteliğinde
        //.passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder())  // şifreleme yöntemi belirleme
        .contextSource()
        //.url(url)  // uzak LDAP sunucusu
        //.port(389)  // uzak LDAP port numarası
        //.ldif("liste.ldif") // sadece belirli bir ldif
        .root("dc=yusufsezer,dc=com");

LDAP bir protokol olduğundan Active Directory gibi bazı uygulamaları tam olarak LDAP kurallarını uygulamayabilir veya farklı kullanımı sahiptir.

Aşağıdaki örnek Active Directory LDAP sunucu işlemleri için kullanılabilir.

auth.ldapAuthentication()
        .userSearchFilter("(&(objectClass=user)(UserPrincipalName={0}))")  // Active directory özel arama
        .contextSource()
        .url("ldap://yusufsezer.com") // ldap adresi
        .root("dc=yusuf,dc=sezer");  // dc bilgileri

Spring Security Active Directory tarafından sağlanan özel hata gibi özellikleri destekleyen AuthenticationProvider arayüzünün bir uygulması olan ActiveDirectoryLdapAuthenticationProvider sınıfına sahiptir.

Sınıf bean tanımı yapılarak aşağıdaki gibi kullanılabilir.

@Bean
public AuthenticationProvider activeDirectoryLdapAuthenticationProvider() {
    ActiveDirectoryLdapAuthenticationProvider provider;
    provider = new ActiveDirectoryLdapAuthenticationProvider("yusufsezer.com", "ldap://yusufsezer.com", "dc=com,dc=yusufsezer");
    provider.setConvertSubErrorCodesToExceptions(true);  // Active Directory özel hataları gösterir
    return provider;
}

NOT: Mevcut LDAP ayarlarının öğrenilmesinde görsel araçların kullanımı faydalı olacaktır.

Spring Security varsayılan olarak sağladığı doğrulama hizmetleri, genişletilebilir doğrulama sağlayıcısı (AuthenticationProvider), kullanıcı servisi (UserDetailsService) ile farklı veya özel doğrulama servislerini oluşturmayı, kullanmayı sağlar.

PasswordEncoder – Şifreleme

Veri kaynağına saklanan kullanıcı bilgilerinin saldırganlar tarafında ele geçirilmesinin önüne geçmek için bilgilerin şifrelenmesi yöntemi kullanılır.

Şifreli bilgilerin girilen bilgiler ile karşılaştırılması için girilen bilgilerin şifrelenerek şifreli bilgiyle karşılaştırılması gerekir.

Spring Security PasswordEncoder arayüzü ve arayüzü uygulayan sınıflar ile farklı şifreleme yöntemlerini destekler.

Düz metin için NoOpPasswordEncoder, MD5 için MessageDigestPasswordEncoder gibi sınıflar yer alır.

Düz metin ve hash tabanlı şifreleme çeşitli yöntemlerle ele geçirilebildiği için Hash şifreleme ile birlikte salt olarak adlandırılan hash şifrelerine ön ve son ek ekleyerek kullanılan şifreleme yöntemleri tavsiye edilmektedir.

Daha önceden şifrelenmiş verileri farklı veri şifreleme formatına çevirme imkanı olmadığından Spring Security farklı şifreleme yöntemlerini sağlamak için DelegatingPasswordEncoder sınıfını sağlar.

Sınıf kullanılacak şifreleme yöntemlerini aşağıdaki gibi yapılandırarak oluşturulur ve kullanır.

@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = "bcrypt";
    Map encoders = new HashMap<>();
    encoders.put(idForEncode, new BCryptPasswordEncoder());
    encoders.put("noop", NoOpPasswordEncoder.getInstance());
    encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}

Daha önceden kullanılan şifrelemenin önüne ön ek olarak {noop}, {MD5} gibi değerlerin eklenmesi gerekir.

NOT: Şifreleme yöntemini yukarıdaki gibi tanımlamak yerine PasswordEncoderFactories#createDelegatingPasswordEncoder metodu kullanılabilir.

Authorization – Yetkilendirme

Spring Security rol temelli yetkilendirme yapmaya, çeşitli ayarlar yaparak sayfalara, metotlara veya sayfanın bir bölümüne erişimeye imkan verir.

Spring Security varsayılan olarak WebSecurityConfigurerAdapter#configure metodu ile sadece giriş yapmış kullanıcıların erişimine izin verir.

// sadece giriş yapmış kullanıcılar
http.authorizeRequests((requests) -> requests.anyRequest().authenticated());
http.formLogin();  // form giriş yöntemi
http.httpBasic();  // Basic Authentication yöntemi

Yetkilendirme işlemi authorizeRequests metodu ile aşağıdaki gibi düzenlenebilir.

http.authorizeRequests()
        // resources, register, about sayfalarına herkes erişebilir
        .antMatchers("/resources/**", "/register", "/about").permitAll()
        // sadece ADMIN rolüne sahip
        .antMatchers("/admin/**").hasRole("ADMIN")
        //.antMatchers("/admin/**").hasAuthority("ADMIN")
        // giriş yapmış kullanıcılar
        .antMatchers("/members/**").authenticated()
        //  ADMIN ve DBA rolüne sahip kullanıcılar
        .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
        // sadece belirli IP sahip kullanıcılar
        .antMatchers("/tr/**").hasIpAddress("88.168.0/24")
        // diğer tüm istekler erişime kapalı
        .anyRequest().denyAll();

Kullanılan metotlar ve karşılıkları aşağıdaki gibidir.

  • antMatchers – ant biçiminde adres eşleşmesi yapar
  • mvcMatchers – Spring MVC paketi ile antMatchers metoduna göre daha kesin ifadeler
  • regexMatchers – düzenli ifade ile adres eşleme
  • hasRole – role sahip, hasAuthority ile benzer
  • hasAuthority – yetkiye sahip, hasRole ile benzer
  • hasAnyRole – herhangi bir role sahip, hasAnyAuthority ile benzer
  • hasAnyAuthority – herhangi bir yetkiye sahip, hasAnyRole ile benzer
  • permitAll – erişime izin ver
  • denyAll – erişimi engelle
  • authenticated – giriş yapmış kullanıcılara izin ver
  • anonymous – anonim kullanıcılara izin ver
  • rememberMe – beni hatırla ile giriş yapan kullanıcılara izin ver
  • fullyAuthenticated – sadece giriş yapan kullanıcılar (beni hatırla girişlere izin vermez)
  • hasIpAddress – sadece belirli ip adresine sahip kullanıcılara izin ver
  • access – parametre olarak gönderilen (Expression) ifadeyi sağlayan kullanıcılara izin ver
    • örnek access parametresi; hasRole(‘admin’) and hasIpAddress(‘88.168.1.0/24’)

Metot temelli yetkilendirme

Spring Security sağladığı sayfa temelli yetkilendirmenin yanında metot temelli veya programsal temelli yetkilendirmeyi sağlar.

Metot temelli yetkilendirme temel olarak aşağıda yer alan Java Annotations ifadelerinin kullanımından ibarettir.

  • @Secured
  • @RolesAllowed – JSR-250
  • @PreAuthorize
  • @PreFilter
  • @PostAuthorize
  • @PostFilter

Metot temelli ifadelerin aktif olması için @EnableGlobalMethodSecurity ifadesi ile etkileştirilmesi gerekir.

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
  // Spring Security ayarları
} 

NOT: Kullanılan ifadye göre @Secured için securedEnabled, @RolesAllowed için jsr250Enabled ve diğerleri için prePostEnabled değerinin true olması gerekir.

İfade proje içerisinde yer alan metot içerisinde kullanılır.

@GetMapping("/admin")
@Secured({"ROLE_ADMIN"})  // @Secured({"ROLE_ADMIN", "ROLE_MOD"})
public ResponseEntity<?> admin() {
    return ResponseEntity.ok("Admin");
}

Spring Security tarafından sağlanan @Secured yerine JSR-250 şartnamesi ile sağlanan @RolesAllowed ifadesi kullanılabilir.

@GetMapping("/admin")
@RolesAllowed({"ROLE_ADMIN"})  // @RolesAllowed({"ROLE_ADMIN", "ROLE_MOD"})
public ResponseEntity<?> admin() {
    return ResponseEntity.ok("Admin");
}

Diğer ifadeler @Secured ve @RolesAllowed ifadelerinden farklı olarak metot çalıştırılmadan veya çalıştırıldıktan sonra Spring Security tarafından sağlanan Expression-Language olarak adlandırılan ifadelerin kullanımını destekler.

@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")  // @PostAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> admin() {
    return ResponseEntity.ok("Admin");
}

İfadelerin önemli bir özelliği ise metot parametrelerini kullanarak işlem yapabilmesidir.

@GetMapping("/admin")
@PreAuthorize("#u.name == authentication.name")
public ResponseEntity<?> admin(@P("u") User user) {
    return ResponseEntity.ok("Admin");
}

Yukarıdaki örnekte parametre olarak gönderilen nesnenin name alanı ile giriş yapan kullanıcının name alanı aynı ise metot çalışacaktır.

Özellik Spring Data tarafından sağlanan @Param ile birlikte aşağıdaki gibi çalışabilir.

@PreAuthorize("#n == authentication.name")
User findUserByName(@Param("n") String name);

@PreFilter ve @PostFilter ifadeleri parametre olarak gönderilen koleksiyon, dizi veya stream değerlerini belirtilen ifadeye göre filtrelemek için kullanılır.

Büyük veriler üzerinde işlem yaparken performans kaybına neden olduğundan kullanımı tavsiye edilmemektedir.

Metot temelli yetkilendirme işlemi sonrası yetkisiz olarak metoda erişildiğinde Spring Security içsel olarak istisna fırlatacak ve HTTP 403 hatası verecektir.

Spring Security tarafından oluşan istisnayı yönetmek, farklı bir sayfa göstermek için HttpSecurity sınıfında yer alan exceptionHandling metodu ile elde edilen ExceptionHandlingConfigurer sınıfında yer alan metotlar kullanılarak özelleştirilebilir.

@Override
protected void configure(HttpSecurity http)
        throws Exception {
    super.configure(http);
    http.exceptionHandling()
            .accessDeniedPage("/erisim-yok");
}

NOT: Metot temelli yetkilendirme sadece Spring IoC tarafından oluşturulan sınıf metotlarında kullanılması ve her metot için ayrıca belirtilmesi gerektiğinden AOP gibi yöntemlerle daha esnek bir şekilde kullanılabilir.

Customization – Özelleştirme

Spring Security sağladığı geniş doğrulama, yetkilendirme ve şifreleme özelliklerinin yanında kullanıcı giriş sayfası, beni hatırla, cors, csrf gibi ayarları özelleştirmeyi sağlar.

Kullanıcı girişini özelleştirme

Kullanıcı adı ve şifre yöntemine göre giriş formu varsayılan olarak Spring Security tarafından oluşturulur.

@Override
protected void configure(HttpSecurity http)
        throws Exception {
    // tüm istekler için yetkilendirme gerekir
    http.authorizeRequests().anyRequest().authenticated();
    http.formLogin();  // varsayılan form
}

Varsayılan giriş formunu özelleştirmek için http.formLogin metodu üzerinden elde edilen FormLoginConfigurer sınıfı kullanılır.

Sınıf varsayılan olarak kullanıcı adı alanı için username, kullanıcı şifre alanı için password parametrelerini kullanır.

Varsayılan kullanıcı alanını değiştirmek için usernameParameter metodu, kullanıcı şifre alanını değiştirmek için passwordParameter metodu kullanılır.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre");

Değişiklik sonrası görünümde bir değişiklik olmayacak üretilen HTML kodlarındaki alanlar ve Spring Security işlemleri değişecektir.

Spring Security tarafından olarak oluşturulan form görünümünü ve adresi (/login) özelleştirmek için loginPage metodu kullanılır.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre")
        .loginPage("/giris")
        .permitAll();

Belirlenen ayara göre /giris adresinde yer alan formda kullaniciadi, kullanicisifre ve CSRF alanı olması gerekir.

<form method="post">
    <%--${param.error} hata işlemlerini yönetmek--%>
    <%--${param.logout} çıkış işlemini yönetmek--%>
    <p>
        <label for="kullaniciadi">Kullanıcı adı:</label>
        <input type="text" id="kullaniciadi" name="kullaniciadi" required="required" autofocus="autofocus" />
    </p>
    <p>
        <label for="kullanicisifre">Kullanıcı şifre:</label>
        <input type="text" id="kullanicisifre" name="kullanicisifre" required="required" />
    </p>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="submit" value="Giriş Yap" />
</form>

Form sayfasında hatalı giriş için ?error parametresi, giriş yaptıktan sonra çıkış yaptığında ?logout parametresi kontrol edilerek mesaj verilebilir.

Varsayılan olarak aktif gelen CSRF bilgisine ulaşmak içim _csrf kullanılır.

NOT: CSRF değeri/alanı gönderilmediğinde/olmadığında giriş yapılmayacaktır.

Tavsiye edilmemesi rağmen CSRF değerini devre bırakmak için csrf().disable() metodu kullanılabilir.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre")
        .loginPage("/giris")
        .permitAll()
        .and()
        .csrf().disable();

NOT: CsrfConfigurer sınıfında yer alan metotlar kullanılarak CSRF işlemi özelleştirilebilir.

Giriş işlemi varsayılan olarak loginPage metodu ile belirtilen adresin POST yöntemine göre çağrılmasıyla olacaktır.

Varsayılanı değiştirmek için loginProcessingUrl metodu ve form etiketinin action özelliğinin değiştirilmesi yeterli olacaktır.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre")
        .loginPage("/giris")
        .loginProcessingUrl("/giris-yap")
        .permitAll()
        .and()
        .csrf().disable();

Hatalı girişler loginPage metodu ile belirtilen adrese ?error parametresi eklenerek yönlendirilecektir.

Başarılı girişler uygulamanın kök dizinine yönlenecektir.

Hatalı ve başarılı girişleri farklı sayfalara yönlendirmek için failureUrl ile defaultSuccessUrl metotları kullanılabilir.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre")
        .loginPage("/giris")
        .loginProcessingUrl("/giris-yap")
        .failureUrl("/giris-hatasi")
        .defaultSuccessUrl("/giris-basarili", true)
        .permitAll()
        .and()
        .csrf().disable();

Spring Security hatalı giriş türüne göre işlem yapmak için failureHandler metodunu, kullanıcı bilgisine göre başarılı giriş işlemini yapmak için successHandler metotları kullanılabilir.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre")
        .loginPage("/giris")
        .loginProcessingUrl("/giris-yap")
        .failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest request,
                    HttpServletResponse response,
                    AuthenticationException exception)
                    throws IOException, ServletException {

                response.setContentType("text/html; charset=UTF-8");
                PrintWriter out = response.getWriter();
                out.println("Yanlış yaptın <br />");
                out.println(exception);

            }
        })
        //.failureUrl("/giris-hatasi")
        .defaultSuccessUrl("/basarili", true)
        .permitAll()
        .and()
        .csrf().disable();

AuthenticationFailureHandler ve AuthenticationSuccessHandler arayüzleri tek metoda sahip olduğundan Java 8 ile gelen Lambda ifadeleri kullanılabilir.

http.formLogin()
        .usernameParameter("kullaniciadi")
        .passwordParameter("kullanicisifre")
        .loginPage("/giris")
        .loginProcessingUrl("/giris-yap")
        .failureUrl("/giris-hatasi")
        .successHandler((request, response, authentication) -> {

            RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            boolean isAdmin = authorities.containsAll(
                    AuthorityUtils.createAuthorityList("ROLE_ADMIN")
            );
            if (isAdmin) {
                redirectStrategy.sendRedirect(request, response, "/admin");
                return;
            }
            redirectStrategy.sendRedirect(request, response, "/");

        })
        //.defaultSuccessUrl("/basarili", true)
        .permitAll()
        .and()
        .csrf().disable();

Çıkış işlemini özelleştirme

Kullanıcı çıkış işlemi Spring Security tarafından /logout adresi üzerinden yapılır.

Varsayılan giriş formunu özelleştirmek için http.logout metodu üzerinden elde edilen LogoutConfigurer sınıfı kullanılır.

@Override
protected void configure(HttpSecurity http)
        throws Exception {
    super.configure(http);
    http.csrf().disable();
    http.logout()
            .logoutUrl("/cikis-yap")
            .logoutSuccessUrl("/cikis-basarili")
            //.addLogoutHandler(logoutHandler)  // çıkış işlemiyle çalışacak metotlar
            //.clearAuthentication(true)  // yetkiyi kaldır
            //.invalidateHttpSession(true)  // sesionları kaldır
            //.deleteCookies(cookieNamesToClear)  // çıkış işlemi ile çerezleri sil
            //.logoutRequestMatcher(logoutRequestMatcher) // sadece belirli istekler için çıkış işlemini yap
            .logoutSuccessHandler((request, response, authentication) -> {
                String name = authentication.getName();
                System.out.println(String.format("%s çıkış yaptı", name));
            });
}

Çıkış işlemi özelleştirilerek çıkış işlemini kayıt altına alma(loglama) gibi işlemler yapılabilir.

Beni hatırla özelleştirme

Kullanıcılar giriş yaptıktan ve tarayıcılarını kapattıktan sonra doğrulama bilgileri silenecektir.

Beni hatırla özelliği belirlenen bir süre kadar kullanıcıların tarayıcılarını kapatsalar bile cihazlarında saklanan bilgiler (çerez-cookie) üzerinden erişimini sağlar.

Beni hatırla özelliğini aktif etmek için http.rememberMe() metodu kullanılır.

@Override
protected void configure(HttpSecurity http)
        throws Exception {
    super.configure(http);
    http.rememberMe();  // beni hatırla özelliği aktif
}

Beni hatırla özelliğini özelleştirmek için http.rememberMe metodu üzerinden elde edilen RememberMeConfigurer sınıfındaki metotlar kullanılır.

http.rememberMe()
        .rememberMeCookieName("beni-hatirla") // cookie adı
        .rememberMeParameter("hatirla-beni")  // HTML checkbox adı
        //.rememberMeCookieDomain(rememberMeCookieDomain)  //  domain adı
        //.alwaysRemember(true)  // her zaman beni hatırla çalışır
        //.useSecureCookie(true)  // sadece HTTPS
        .tokenValiditySeconds(60 * 60);  // beni hatırla süresi

Varsayılan olarak beni hatırla bilgileri çerez-cookie’de (kullanıcı cihazında yer alan dosyalarda) saklanır.

Kullanıcı cihazında yer alan çerezi saldırgan ele geçirerek şifrelenmiş bilgiye ulaşılabilir.

Çerezlerin saldırgan tarafından alınarak bilgilere erişimin önüne geçmek için token temelli yöntem kullanılabilir.

Token temelli yöntemde kullanıcı giriş yaptığında giriş yapan kullanıcı bilgileri veritabanında saklanır.

Kullanıcı saklanan bilgilere doğrudan erişemez, sadece cihazına çerez ile saklanan token sayesinde bu bilgileri giriş yapmak için kullanılmasını sağlar.

InMemoryTokenRepositoryImpl token bilgilerini Java Map koleksiyonunda uygulama çalıştığı sürece saklar.

http.rememberMe()
        .tokenRepository(new InMemoryTokenRepositoryImpl());

NOT: in-Memory test/deneme amçlı kullanılmaktadır.

JdbcTokenRepositoryImpl token bilgilerini JDBC kullanarak ilişkisel veritabanında saklamak için kullanılır.

@Override
protected void configure(HttpSecurity http)
        throws Exception {
    super.configure(http);
    http.rememberMe()
            .tokenRepository(this.persistentTokenRepository());
}

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl = new JdbcTokenRepositoryImpl();
    jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);  // tablo oluştur?        
    jdbcTokenRepositoryImpl.setDataSource(this.dataSource());  // bağlantı bilgileri
    return jdbcTokenRepositoryImpl;
}

Farklı veri kaynaklarını kullanmak için PersistentTokenRepository arayüzü kullanılabilir.

Beni hatırla özelliğini kullanırken çerez adı, çerez sonlanma tarihi, çerez domain bilgisi, sadece https isteklerinde aktif olma gibi kısıtlamarın eklenmesi faydalı olacaktır.

NOT: Beni hatırla özelliğinin kullanımı gerekmedikçe tavsiye edilmemektedir.

CSRF özelleştirme

Varsayılan olarak aktif gelen CSRF özelliğini devre dışı bırakmak için CsrfConfigurer sınıfında yer alan disable metodu kullanılır.

http.csrf();

Sadece belirli bir adreste devre dışı bırakmak için ignoringAntMatchers metodu kullanılabilir.

http.csrf().ignoringAntMatchers("/sayfalar");

Belirli bir isteğe göre devre dışı bırakmak için ignoringRequestMatchers metodu kullanılabilir.

http.csrf()
        .ignoringRequestMatchers(
                (request) -> "XMLHttpRequest".equals(request.getHeader("X-Requested-With"))
        );

Yukarıdaki örnekte X-Requested-With değeri XMLHttpRequest olan istekte CSRF devre dışı kalacaktır.

Sadece belirli bir istek için aktif etmek için requireCsrfProtectionMatcher metodu kullanılabilir.

http.csrf()
        .requireCsrfProtectionMatcher(
                (request) -> !"XMLHttpRequest".equals(request.getHeader("X-Requested-With"))
        );

Yukarıdaki örnekte X-Requested-With değeri XMLHttpRequest olmayan istekte CSRF aktif olacaktır.

NOT: CSRF güvenlik önleminin devre dışı bırakılması tavsiye edilmemektedir.

CORS özelleştirme

CORS özelliğini devre dışı bırakmak için CorsConfigurer sınıfında yer alan disable metodu kullanılır.

http.cors().disable();

CORS özelleğini özelleştirmek için CorsConfiguration sınıfındaki metotlar kullanılarak özelleştirilmesi, configurationSource metoduna parametre olarak gönderilmesi veya bean tanımının yapılması yeterli olacaktır.

@Override
protected void configure(HttpSecurity http)
        throws Exception {
    super.configure(http);
    http.cors().configurationSource(this.corsConfigurationSource());
}

CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("https://www.yusufsezer.com"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST"));
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

Spring Security ile birlikte Spring MVC kullanıldığında, Spring MVC CORS özelliğini yönetecektir.

Diğer özelleştirmeler

Spring Security sağladığı özellikleri özelleştirmek için HttpSecurity sınıfındaki metotlar üzerinden elde edilen metotlar kullanılabilir.

http.headers().frameOptions().disable()
        .and()
        .headers().cacheControl().disable()
        .and()
        .csrf().disable();
        //.jee().mappableRoles(mappableRoles)

Servlet desteği

Spring Security Servlet temelli olarak çalıştığından Servlet tarafından sağlanan yetkilendirme özelliklerini kullanabilir ve destekler.

Kullanıcı bilgilerine HttpServletRequest arayüzünde yer alan getUserPrincipal veya getRemoteUser metodu üzerinden erişim sağlanır.

@WebServlet(name = "OrnekServlet", urlPatterns = {"/"})
public class OrnekServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        calistir(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        calistir(request, response);
    }

    protected void calistir(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        Principal principal = request.getUserPrincipal();
        String username = principal.getName();  // veya request.getRemoteUser()
        response.getWriter().println(String.format("Merhaba %s", username));

    }

}

Kullanıcı girişini kontrol etmek için authenticate metodu kullanılır.

boolean isLogged = request.authenticate(response);

Kullanıcı rollerine erişmek için isUserInRole metodu kullanılır.

boolean isAdmin = request.isUserInRole("ADMIN");  // veya ROLE_ADMIN

Çıkış yapmak için logout metodu kullanılır.

request.logout();

Çıkış işlemi sonrası, sayfayı yönlendirmek faydalı olackatır.

Giriş yapmak için login metodu kullanılır.

request.login(username, password);

Oturumu bilgisini değiştirmek için changeSessionId metodu kullanılır.

String newSessionId = request.changeSessionId();

Servlet tabanlı uygulamaların Spring Security ile sıkı bağlı olmasını engellemek için Servlet tarafından sağlanan özelliklerin kullanımı faydalı olacaktır.

Spring MVC desteği

Spring Security kurulumu başlığında yer alan AbstractSecurityWebApplicationInitializer soyut sınıfını kurucu ile kullanmak hataya neden olacaktır.

Spring MVC ile birlikte Spring Security kullanmak için sınıfın kurucusuz olarak eklenmesi gerekir.

public class SecurityInitializer
        extends AbstractSecurityWebApplicationInitializer {}

Spring Security ayarları Spring MVC ile yüklenerek kurulum tamamlanır.

public class Initializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{
            SecurityConfig.class  // Spring Security ayarları
        };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{
            WebConfig.class  // Spring MVC ayarları
        };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

}

Spring MVC, Spring Security tarafından sağlanan CORS, CSRF gibi bir çok güvenlik özelliğini otomatik olarak aktif eder.

Spring Security yetkilendirme için kullanılan antMatchers metodunda daha hassas mvcMatchers kullanımını sağlar.

http.authorizeRequests()
        .mvcMatchers("/admin").hasRole("ADMIN");

Spring MVC /admin eşleşmesi yerine aynı adrese-metoda /admin.html gibi bir eşlemeyi yaptığında antMatchers /admin.html adresine izin verirken mvcMatchers metodu izin vermeyecektir.

@AuthenticationPrincipal

Spring MVC AuthenticationPrincipalArgumentResolver sınıfı sayesinde kullanıcı bilgilerini i eşlemeyi sağlar.

@GetMapping
public ResponseEntity<?> bilgiler(@AuthenticationPrincipal User user) {
    String username = user.getUsername();
    boolean isEnabled = user.isEnabled();
    return ResponseEntity.ok(username + " - " + isEnabled);
}

Kullanıcı bilgilerine ulaşmak için @AuthenticationPrincipal ifadesi ve kullanıcı bilgilerinin yer aldığı sınıf(User) kullanılır.

Spring MVC kullanıcı bilgilerini almak için Authentication#getPrincipal() metodu sonucu oluşan sınıfı kullanır.

Metot dönüş değeri dinamik olduğundan dolayı UserDetails arayüzünü uygulayan özel-farklı sınıf kullanılabilir.

Spring MVC tarafından sağlanan JSP etiketleri Spring Security tarafından sağlanan CSRF özelliğini dinamik olarak sağlar.

JSP etiket desteği

Spring Security Web, Servlet temelli olarak çalıştığı için Servlet desteğinin yanında JSP desteğinide sağlar.

Spring Security SecurityContextHolder sınıfı üzerinden kullanıcı bilgilerine ulaşılabilir.

<h1>Merhaba <%= SecurityContextHolder.getContext().getAuthentication().getName() %>!</h1>

JSP etiketleri ile kullanıcı bilgileri erişmek için request nesnesi kullanılabilir.

<h1>Merhaba <%=request.getUserPrincipal().getName() %>!</h1>

JSP EL ile kullanıcı bilgilerine erişmek için pageContext nesnesi kullanılabilir.

<h1>Merhaba ${pageContext.request.userPrincipal.name}!</h1>

Java JSP hakkında detaylı bilgi almak için Java JSP yazıma bakmalısın.

Spring Security içerisinde yer alan JSP etiketleri kullanıcı bilgilerini almayı, sayfaki bölümlerin sadece belirtilen şartı sağlayan kullanıcı tarafından görünmesi sağlanır.

Etiketlerin kullanabilmesi için Spring Security JSP etiketlerine ait paketin pom.xml dosyasına eklenmesi gerekir.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.4.2</version>
</dependency>

Etiketleri kullanmak için aşağıdaki taglib direktifini belirtmek yeterli olacaktır.

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

authentication etiketi

Kullanıcı bilgilerine (Authentication örneğine) erişim için kullanılır.

<h1>Merhaba <sec:authentication property="principal.Username" />!</h1>

authorize etiketi

Etiket içerisinde yer alan bölümü sadece access etiketi ile belirtilen (Expression) şartı sağlayan kullanıcıların görebileceği içerikler için kullanılır.

<sec:authorize access="hasRole('ADMIN')">
    Burayı sadece ADMIN rolüne/yetkisine sahip kişi görebilir.
</sec:authorize>

Etiketin diğer önemli özelliğ ise url özelliği sayesinde belirtilen url’ye erişime olan kullanıcıların belirtilen bölümü görmesidir.

<sec:authorize url="/admin">
    Burayı sadece /admin adresine erişimi olan kullanıcı görebilir.
</sec:authorize>

csrfInput etiketi

Farklı sayfalardan gönderilen istekleri önlemek için kullanılan CSRF güvenlik önlemi için CSRF değerinin yer aldığı gizli giriş(input) etiketi oluşturur.

<sec:csrfInput />

Etiket form hazırlarken kolaylık sağlar.

csrfMetaTags etiketi

CSRF güvenliğini sağlamak için meta etiketi oluşturur.

<head>
    <sec:csrfMetaTags />
</head>

HTML etiketlerini kullanmak yerine JSP etiketlerini kullanmak olası hataları önleyeceğinden kullanımı faydalı olacaktır.

Thymeleaf gibi görüntüleme motorları (template-view engine) benzer işlemleri yapan etiketlere sahiptir.

Diğer

Spring Security sağladığı geniş, esnek ve genişletilebilir güvenlik yapısı sayesinde varsayılan olarak gelen güvenlik özelliklerini özelleştirmeyi, yeni güvenllik yöntemleri geliştirmeyi sağlar.

Form girişi gibi temel güvenlik yöntemlerini yapmak kolay olsa da diğer güvenlik önlemleri, güvenlik önleminin doğası gereği karmaşık olabilir.

Karmaşık/Diğer güvenlik önlemlerini almak için öncelikle güvenlik önleminin çalışması ile ilgili detaylı bilgi almak faydalı olacaktır.

Spring Security detaylı hata, istisna mesajlarını saldırganların güvenlik önlemleriyle ilgili bilgi almasının önüne geçmek için gizlemektedir.

Detaylı hata, istisna mesajlarına erişimek için günlük(log) kayıtlarına bakmak veya uygulamayı DEBUG modunda çalıştırarak adım adım takip etmek faydalı olacaktır.

Uygulama geliştirirken JCP ve JEE tarafından belirlenen standartları kullanmak platform değişikliği, beklenmedik değişikliklere karşı faydalı olacaktır.

Java Derslerine buradan ulaşabilirsiniz.

Hayırlı günler dilerim.


Bunlara'da bakmalısın!