安全——Spring Security

目錄

1.簡單安全認證

1.1 簡單實現

1.2 實現原理

2.使用WebSecurityConfigurerAdapter自定義

2.1 自定義用戶服務信息

2.1.1 使用內存簽名服務

2.1.2 使用數據庫定義用戶認證服務

2.1.3 使用自定義用戶認證服務

2.2 限制請求

2.2.1 配置請求路徑訪問權限

2.2.2 使用Spring表達式配置權限訪問

2.2.3 強制使用HTTPS


在Web應用中,對於客戶端的請求需要考慮安全性問題,例如,對於一些重要的操作,有些請求需要用戶驗明身份以後纔可以進行。這樣做的意義在於保護自己的網站安全,避免一些惡意攻擊導致數據和服務的不安全。

爲了提供安全的機制,Spring提供了其安全框架Spring Security,它是一個能夠爲基於Spring生態圈,提供安全訪問控制解決方案的框架。它提供了一組可以在Spring應用上下文中配置的機制,充分利用了Spring的強大特性,爲應用程序提供聲明式的安全訪問控制功能,減少了爲企業系統安全控制編寫大量重複代碼的工作。

1.簡單安全認證

在Web工程中,一般使用Servlet過濾器(Filter)對請求進行攔截,然後在Filter中通過自己的驗證邏輯來決定是否放行請求。同樣地,Spring Security也是基於這個原理,在進入到DispatcherServlet前就可以對Spring MVC的請求進行攔截,然後通過一定的驗證,從而決定是否放行請求訪問系統。

在SpringBoot中,我們只需要引入security的starter,它便會自動啓動Spring Security。

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
     </dependency>

1.1 簡單實現

引入Spring Security的starter之後,我們直接啓動SpringBoot,會在控制檯打印出隨機生成的密碼:

2020-02-04 16:39:49.157  INFO 10504 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: e8a6814a-8bb1-497e-a0e3-4aefc757aeb6

這裏需要注意的是,密碼是隨機生成的,也就是說每次啓動密碼都會不一樣。我們訪問一下我們的Web,會出現如下登錄頁面:

我們輸入用戶名“user”,輸入隨機生成的密碼“e8a6814a-8bb1-497e-a0e3-4aefc757aeb6”,然後點擊登錄,就可以征程跳轉到我們的請求路徑上。當然我們也可以自定義我們的用戶名和密碼,這樣就不需要每次啓動的時候都隨機生成一個密碼。我們在application.properties中加入如下代碼:

#自定義用戶名和密碼
spring.security.user.name=myuser
spring.security.user.password=123456

這樣我們就定義了使用用戶名“myuser”和密碼“123456”來登錄我們的系統了。除了這兩個配置外,Spring Boot還支持多個Spring Security的配置選項。在實際的工作中,大部分選項無需進行配置,只需要配置少量的內容即可。

1.2 實現原理

爲了後續學習,我們稍微討論一下Spring Security的原理。一旦啓用了Spring Security,IOC容器就會創建一個類型爲FilterChainProxy,名稱爲springSecurityFilterChain的Spring Bean。FilterChainProxy實現了Filter接口,因此它是一個特殊的攔截器。在Spring Security操作的過程中它會提供Servlet過濾器DelegatingFilterProxy,這個過濾器會通過Spring Web IOC容器去獲取Spring Security所自動創建的FilterChainProxy對象,這個對象上存在一個攔截器列表,列表上存在用戶驗證的攔截器、跨站點請求僞造等攔截器,這樣它就可以提供多種攔截功能。通過FilterChainProxy,我們還可以註冊自定義的Filter來實現對應的攔截邏輯,以滿足不同的需求。當然,Spring Security也實現了大部分常用安全功能,並提供了相應的機制來簡化開發者的工作,所以大部分情況下並不需要自定義開發。

2.使用WebSecurityConfigurerAdapter自定義

前面討論了FilterChainProxy對象,過濾器DelegatingFilterProxy的攔截邏輯就是根據它的邏輯來完成的。爲了給FilterChainProxy對象加入自定義的初始化,Spring提供了專門的接口WebSecurityConfigurer,並且在這個接口的定義上提供了一個抽象類WebSecurityConfigurer Adapter。開發者通過繼承它就能得到Spring Security默認的安全功能,也可以通過覆蓋它提供的方法,來自定義自己的安全攔截方案。

下面我們研究一下WebSecurityConfigurerAdapter中默認存在的3個方法,它們是:

//用來配置用戶信息服務,還可以給用戶賦予角色
protected void configure(AuthenticationManagerBuilder auth);
//用來配置Filter鏈,默認是一個空實現
protected void configure(HttpSecurity http);
//用來配置攔截保護的請求,比如什麼請求放行,什麼請求需要驗證
public void configure(WebSecurity web);

對於AuthenticationManagerBuilder 參數的方法,則是定義用戶(user)、密碼(password)和角色(Role);對於使用WebSecurity參數的方法,主要是配置Filter鏈的內容,可以配置Filter鏈忽略哪些內容,WebSecurityConfigurerAdapter提供的是空實現,沒有任何的配置;對於HttpSecurity參數的方法,則是指定用戶和角色與對應URL的訪問權限,也就是開發者可以通過覆蓋這個方法來指定用戶或者角色的訪問權限。在WebSecurityConfigurerAdapter提供的驗證方式下滿足通過用戶驗證或者HTTP基本驗證的任何請求,Spring Security都會放行。

2.1 自定義用戶服務信息

在WebSecurityConfigurerAdapter中的方法

protected void configure(AuthenticationManagerBuilder auth);

是一個用於配置用戶信息的方法,在Spring Security中默認是沒有任何用戶配置的。而在Spring Boot中,如果沒有用戶的配置,它將會自動生成一個名稱爲user,密碼隨機的用戶。我們可以通過覆寫該方法定義用戶、密碼和角色權限,實現方式主要包含使用內存簽名服務、數據庫簽名服務和自定義簽名服務。

2.1.1 使用內存簽名服務

顧名思義就是將用戶的信息存放在內存中,相對而言,它比較簡單,適合於測試的快速環境搭建,實例代碼如下:

package com.martin.config.security;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author: martin
 * @date: 2020/2/4
 */
@EnableWebSecurity
public class MemoryConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密碼編碼器
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        //使用內存存儲並設置密碼編碼器
        auth.inMemoryAuthentication().passwordEncoder(passwordEncoder)
                //註冊用戶admin,密碼爲admin,並賦予USER和ADMIN兩種角色權限
                .withUser("admin").password(passwordEncoder.encode("admin")).roles("USER","ADMIN")
                .and().withUser("myuser").password(passwordEncoder.encode("123")).roles("USER");
    }
}

在Spring5的Security中要求使用密碼編碼器,否則會發生異常,所以代碼中首先創建了一個BCryptPasswordEncoder實例,這個類實現了PasswordEncoder接口,它採用的是單向不可逆的密碼加密方式。roles方法賦予角色類型,將來就可以通過這個角色名稱賦予權限了,只是Spring Security會在註冊的角色名稱前面增加前綴“ROLE _”。除了代碼中用到方法,UserDetailsBuilder類中還包括其他方法:

2.1.2 使用數據庫定義用戶認證服務

在大部分的情況下,用戶的信息會存放在數據庫中,爲此Spring Security提供了對數據庫的查詢方法來滿足開發者的需要。首先我們創建相關表:角色表(t_role)、用戶表(t_user)、用戶角色表(t_user_role),這裏需要注意的是pwd存放的加密後的密文。

CREATE TABLE `t_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(60) NOT NULL,
  `note` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(60) NOT NULL,
  `pwd` varchar(100) NOT NULL,
  `available` tinyint(1) NOT NULL DEFAULT '1',
  `note` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uni_idx` (`user_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `t_user_role` (
  `id` int(12) NOT NULL AUTO_INCREMENT,
  `role_id` int(12) NOT NULL,
  `user_id` int(12) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uni_role_user` (`role_id`,`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

使用數據驗證的實例代碼如下:

package com.martin.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.sql.DataSource;

/**
 * @author: martin
 * @date: 2020/2/4
 */
@EnableWebSecurity
public class DatabaseConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private DataSource dataSource;
    //使用用戶名稱查詢密碼
    private static final String PWD_QUERY = "select user_name,pwd,available from t_user where user_name=?";
    //使用用戶名稱查詢角色信息
    private static final String ROLE_QUERY = "select u.user_name,r.role_name from t_user u,t_user_role ur,t_role r " +
            "where u.id = ur.user_id and r.id = ur.role_id and u.user_name = ?";
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密碼編碼器
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        auth.jdbcAuthentication().passwordEncoder(passwordEncoder)
                //查詢用戶,自動判斷密碼是否一致,並賦予角色權限
                .dataSource(dataSource).usersByUsernameQuery(PWD_QUERY).authoritiesByUsernameQuery(ROLE_QUERY);
    }
}

代碼中首先使用@Autowired注入了數據源,這樣就能夠使用SQL。其次定義了兩條SQL,其中PWD_QUERY所定義的是根據用戶名查詢用戶信息,ROLE_QUERY是使用角色名去查詢角色信息,這樣就能夠賦予角色。這裏需要注意的是,PWD_QUERY定義的SQL返回了3個列,分別是用戶名、密碼和布爾值。這樣就可以對用戶名和密碼進行驗證了,其中布爾值是判斷用戶是否有效,這裏使用的是available列,它存儲的數據被約束爲1和0,如果爲1則用戶是有效的,否則就是無效用戶。

這裏需要注意的是,雖然通過BCrypt加密的密文很難破解,但是仍舊不可避免的用戶使用類似“123”、“abcd”這樣的簡單密碼,如果被人截取了這些簡單的密碼進行匹配,那麼一些用戶的密碼就可能被別人破解。爲了克服這個問題,在實際的應用中通過自己的祕鑰對密碼進行加密處理,而祕鑰存在企業服務器上,這樣即使密文被人截取,別人也無法得到祕鑰破解密文,這樣就大大提高了網站的安全性。我們先在application.properties中加入一個祕鑰配置屬性:

system.user.password.secret=uvwxyz

對實現代碼進行改造如下:

package com.martin.config.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;

import javax.sql.DataSource;

/**
 * @author: martin
 * @date: 2020/2/4
 */
@EnableWebSecurity
public class DatabaseConfig extends WebSecurityConfigurerAdapter {
    @Value("${system.user.password.secret}")
    private String secret;
    @Autowired
    private DataSource dataSource;
    //使用用戶名稱查詢密碼
    private static final String PWD_QUERY = "select user_name,pwd,available from t_user where user_name=?";
    //使用用戶名稱查詢角色信息
    private static final String ROLE_QUERY = "select u.user_name,r.role_name from t_user u,t_user_role ur,t_role r " +
            "where u.id = ur.user_id and r.id = ur.role_id and u.user_name = ?";

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密碼編碼器
        PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(secret);
        auth.jdbcAuthentication().passwordEncoder(passwordEncoder)
                //查詢用戶,自動判斷密碼是否一致,並賦予角色權限
                .dataSource(dataSource).usersByUsernameQuery(PWD_QUERY).authoritiesByUsernameQuery(ROLE_QUERY);
    }
}

在這段代碼中,使用了Pbkdf2PasswordEncoder創建了密碼編碼器。除此之外,Spring Security還存在BCryptPasswordEncoder和DelegatingPasswordEncoder等密碼編碼器,甚至可以實現PasswordEncoder接口,定義自己的編碼器。

2.1.3 使用自定義用戶認證服務

在如今,有些企業的用戶量很大,使用數據庫進行驗證有時候甚至會造成網站的緩慢,所以有些企業會考慮使用NoSQL存儲用戶數據,如Redis,這樣就能大大地加速用戶的驗證速度。由於需求的多樣化,有時候我們也需要對用戶進行自定義驗證。

假設系統已經存在了UserRoleService的接口,通過它可以操作Redis和數據庫,下面就基於這個基礎進行開發。

實現UserDetailsService接口定義用戶服務類:

package com.martin.config.security.service.impl;

import com.martin.config.security.pojo.Role;
import com.martin.config.security.pojo.UserInfo;
import com.martin.config.security.service.UserRoleService;
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.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;

/**
 * @author: martin
 * @date: 2020/2/5
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private UserRoleService userRoleService;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        UserInfo userInfo = userRoleService.getUserByName(userName);
        List<Role> roleList = userRoleService.findRolesByUserName(userName);

        return change2UserDetails(userInfo, roleList);
    }

    private UserDetails change2UserDetails(UserInfo userInfo, List<Role> roleList) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roleList) {
            GrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleName());
            authorities.add(authority);
        }

        return new User(userInfo.getUserName(), userInfo.getPwd(), authorities);
    }
}

然後我們實現自定義的認證服務類:

package com.martin.config.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;

import javax.annotation.Resource;

/**
 * @author: martin
 * @date: 2020/2/4
 */
@EnableWebSecurity
public class UserDefineConfig extends WebSecurityConfigurerAdapter {
    @Value("${system.user.password.secret}")
    private String secret;
    @Resource(name = "userDetailServiceImpl")
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //密碼編碼器
        PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(secret);
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
}

2.2 限制請求

前面我們只是驗證了用戶,並且給予用戶賦予了不同的角色。如果要實現對於不同的角色賦予不同的訪問權限,我們可以通過覆寫WebSecurityConfigurerAdapter的configure(HttpSecurity http)方法來實現。首先我們看一下該方法的默認實現:

 protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
    }

authorizeRequests方法限定只對登錄成功的用戶請求,anyRequest表示任何的請求,authenticated表示對所有登錄成功的用戶允許方法,and是連接詞,formLogin代表使用Security默認的登錄界面,httpBasic表示啓用HTTP基礎認證。

從源碼中我們可以看出,只需要通過用戶認證就可以訪問所有的請求地址,但這往往不是我們真實的需要。現實中,我們需要根據不同的角色來賦予不同的訪問權限。

2.2.1 配置請求路徑訪問權限

對於Spring Security,它允許使用Ant風格或者正則表達式的路徑限定安全請求,例如下面使用Ant風格的實例代碼:

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/user/welcome","/user/details").hasAnyRole("USER","ADMIN")
                .antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                .anyRequest().permitAll()
                .and().anonymous()
                .and().formLogin()
                .and().httpBasic();
    }

這裏authorizeRequests方法表示設置哪些需要簽名的請求,並且可以將不同的請求權限賦予不同的角色。antMatchers配置的是請求的路徑,這裏使用的是Ant風格的配置,指明瞭請求的路徑。hasAnyRole方法,指定了角色“ROLE_USER”,"ROLE_ADMIN"纔可以訪問。對於“/admin/**”則是統配指定,只有分配了“ROLE_ADMIN”的角色可以訪問。注意hasAnyRole方法會默認加入前綴“ROLE”,而hasAuthority方法不會默認加入前綴“ROLE_”,它們都表示對應的請求路徑只有用戶分配了對應的角色才能訪問。最後,anyRequests方法代表任意的沒有限定的請求,permitAll方法則表示沒有配置過權限限定的路徑全部允許訪問。

and方法表示連接詞,重新加入新的權限規則,這裏配置了anonymous方法,說明允許匿名訪問沒有配置過的請求。

formLogin方法代表啓用默認的登錄頁面,httpBasic方法表示啓用HTTP的Basic認證請求輸入用戶名和密碼。除此之外,還包括如下的權限方法:

這裏需要注意的是,所有的權限配置會採用配置優先的原則,因爲有時候權限會產生衝突。例如上面代碼中,允許匿名訪問,但沒有給出地址,而前面又加入了登錄驗證訪問其他路徑的限制,所以基於配置優先的原則,Security還是會採用登錄驗證訪問其他路徑的限制。

此外,我們也可以使用正則表達式規則來配置:

        http.authorizeRequests().regexMatchers("/user/welcome","/user/details").hasAnyRole("USER","ADMIN")
                .regexMatchers("/admin/.*").hasAuthority("ROLE_ADMIN")
                .and().formLogin()
                .and().httpBasic();

2.2.2 使用Spring表達式配置權限訪問

有時候需要更加強大的驗證功能,而上述功能只能使用方法進行配置,爲了更加靈活,我們還可以使用EL表達式進行配置。這就需要使用access方法,它的參數就是一個EL表達式,如果這個表達式返回true,就允許訪問,否則不允許方法。實現代碼如下:

        http.authorizeRequests()
                //使用Spring表達式限定只有角色ROLE_USER或者ROLE_ADMIN
                .antMatchers("/user/welcome","/user/details").access("hasAnyRole('USER') or hasAnyRole('ADMIN')")
                //設置訪問權限給ADMIN,要求是完整登錄(非記住我登錄)
                .antMatchers("/admin/welcome1").access("hasAuthority('ROLE_ADMIN') && isFullyAuthenticated()")
                //設置訪問權限給ADMIN,允許不完整登錄
                .antMatchers("/admin/welcome2").access("hasAuthority('ROLE_ADMIN')")
                .anyRequest().permitAll()
                //啓用記住我的功能
                .and().rememberMe()
                .and().formLogin()
                .and().httpBasic();

除了代碼中的這些表達式方法以外,Security還提供了其他的方法:

2.2.3 強制使用HTTPS

在一些實際的工作環境中,如銀行、金融公司等,對於銀行賬號、密碼、身份信息等往往都是極爲敏感的,對於這些信息往往需要更爲謹慎地進行保護。通過HTTPS協議採用證書進行加密,就可以保護那些敏感的信息。Spring Security強制使用HTTPS請求的實例代碼如下:

        //使用安全通道,限定爲https請求
        http.requiresChannel().regexMatchers("/admin/.*").requiresSecure()
                //不使用HTTPS請求
                .and().requiresChannel().regexMatchers("/user/welcome", "/user/details").requiresInsecure()
                //限定允許的訪問角色
                .and().authorizeRequests().regexMatchers("/admin/.*").hasAnyRole("ADMIN")
                .and().formLogin()
                .and().httpBasic();

requiresChannel()方法說明使用通道,然後通過正則限定請求,最後使用requiresSecure表示使用HTTPS請求。對於requiresInsecure則是取消安全請求的機制,這樣就可以使用普通的HTTP請求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章