Spring Security一看就會
Spring Security一看就會
簡介 |
Spring已經是我們一般Java Web開發/API開發最常用的架構了,但一提到Spring Security多數還是覺得不容易操作,覺得客製化困難或根本不適用。此處從基本著手,了解Spring Security的設計,一探它簡單優雅的一面,從此安全也可以輕鬆完成。 |
作者 |
夏宏彰 |
1. 前言
l Spring Security其實已提供許多基本的實作方法,我們只要在應用程式適當的配置與整合,不用額外客製,使用複雜的機制與演算法,即可擁有一定程度的安全把關。
l 對Spring Security的設計有了基本的認識後,如何客製,從何客製也將變得清楚,能夠與我們的應用程式完美的整合在一起。
2. 目的
l 了解Spring Security的設計。
l 了解Spring Security如何配置。
3. 開始前準備
本主題說明基於以下版本的環境:
l JDK 1.8以上
l Spring Security 4.2.3 (Current 5.0.4)
l Spring Boot 1.5.4 Release. (Current 2.0.1)
4. Spring Security是什麼?
Spring Security是一個提供認證與授權的軟體架構,使用它不但可免於session fixation, clickjacking, cross site request forgery, 等攻擊,也會自動加上Http security header,且與Spring MVC有良好的整合。
l 認證: 確保使用者人如其名。Spring Security提供認證的機制有Http Basic, Form Based … 等。
l 授權: 確保使用者僅可存取允許的資源。Spring Security提供幾種授權的層級,有http請求、http方法、物件等.
5. Spring Security的設計
5.1、 Spring Security有三個基本的jar檔:
l Core - spring-security-core.jar,只要是用Spring Security就需要此jar。
l Web - spring-security-web.jar,包括filters與web-security的基礎程式。
l Config - spring-security-config.jar,包括namespace configuration與Java configuration程式。
5.2、 Security Filters
Spring Security完全實作在Servlet Filter中,所以要在我們的Web中使用Spring Security,就要將DelegatingFilterProxy Filter配置到Web應用中。Spring配置的方法有幾種,最原始的方法web.xml配置如下:
<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>
*在Spring Framework中,我們已很少再使用此方式設定,在此僅是為了說明Spring Security的基本架構。以下將以Spring Boot用Java Based Configuration (以annotations為輔助進行配置)的方式為例說明Spring Security如何配置。
5.3、 DelegatingFilterPorxy
DelegatingFilterPorxy是Spring Security的入口,我們由下圖看到它其實會帶入更多的filters,這也是Spring為了減少在web.xml上的設定而做的設計。
DelegatingFilterPorxy Class Diagram如下:

這關係很清楚地告訴我們,DelegatingFilterPorxy使用了FilterChainProxy,就會引用其他的filters進行安全相關的檢核作業。原則上每個filter都要配置,但在Spring Boot中我們僅需依我們需要的部分進行配置,其他可省略或用預設值即可。以下說明幾個常用的Filter。
5.4、 SecurityContextPersistenceFilter
這是Spring Security的第一個filter,主要是載入Security Context,在Security Context中我們可以取得使用者的Authentication物件,裡面有所有處理使用者認證/授權的資料。若Security Context不存在則會自動建立一個新的。此filter預設會使用HttpSessionSecurityContextRepository,將Security Context存放在Http Session中。
SecurityContextPersistenceFilter Class Diagram

5.5、 LogoutFilter
執行使用者登出的動作。由下圖可知此filter是呼叫LogoutHandler,預設的SecurityContextLogoutHandler會將此使用者的Security Context清除,並且讓session失效。
LogoutFilter Class Diagram

5.6、 AbstractAuthenticationProcessingFilter
這個Filter就是主要處理認證的地方,其他的filters可以乎略用預設值,只要看這個filter就可初步掌握80%以上我們所要用的Spring Security功能。
先看我們最常用的form-based 認證,也就是UsernamePasswordAuthenticationFilter,在此filter我們會設定form的URL以進行監聽與預設username/password參數名稱來取得認證資料; 另外我們亦允許Http Basic Authentication。此filter會呼叫實作AuthenticationManager介面的ProviderManager,而ProviderManager會再呼叫實作AuthenticationProvider介面的DaoAuthenticationProvider。此DaoAuthenticationProvider會依我們配置的UserDetailService,看是將使用者資料存放在memory (InMemoryDAOImpl),或是database(JdbcDaoImpl)中。也就是說,我們主要需要設定的就是UserDetailsService而已。
以下就是全部Spring Security的Java Configuration,簡單的設定即可
- userDetailsService():用InMemory的方式建立兩個使用者的認證/授權資料。
- configure(HttpSecurity): 設定任何request皆需認證,且指定Form Login page。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user_1").password("123456").authorities("USER").build());
manager.createUser(User.withUsername("user_2").password("123456").authorities("USER").build());
return manager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.httpBasic();
}
}
AbstractAuthenticationProcessingFilter Class Diagram:

AuthenticationManager
認證的入口,只有一個Method:
Authentication authenticate(Authentication authentication) throws AuthenticationException
將認證的資料(Authentication)輸入,並取得認證的結果,若發生不符則拋出AuthenticationException。
Authentication
認證後取得的物件的介面,有 getAuthorities(), getCredentials(), getPrincipal(), isAuthenticated()等方法。
5.7、 使用Database
真的應用還是需要將使用者的資料存放在Database中,而password也得要加密存放才行。Spring Security如何做呢? 全部皆只要設定即可。
依Spring原本設計直接使用其實相當方便,而這也是一般我們設計DB的方式。DB設計User Profile Schema時,一般我們會將使用者的基本資料與id/pwd分開在不同的表格; 而使用者的角色若有需要,也是用另外的表格存放。這樣的設計改用Spring Security時是完全相容的,依序用Spring的users與authorities兩表格即可(其他表格可不用),schema如下:
create table users(
username varchar(50) not null primary key,
password varchar(50) 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));
create unique index ix_auth_username on authorities (username,authority);
是不是很簡潔呢。若就是不能用此表格,解法也不難,只要將以下的sql更改成你的sql即可:
// UserDetailsManager SQL
DEF_CREATE_USER_SQL = "insert into users (username, password, enabled) values (?,?,?)";
DEF_DELETE_USER_SQL = "delete from users where username = ?";
DEF_UPDATE_USER_SQL = "update users set password = ?, enabled = ? where username = ?";
DEF_INSERT_AUTHORITY_SQL = "insert into authorities (username, authority) values (?,?)";
DEF_DELETE_USER_AUTHORITIES_SQL = "delete from authorities where username = ?";
DEF_USER_EXISTS_SQL = "select username from users where username = ?";
DEF_CHANGE_PASSWORD_SQL = "update users set password = ? where username = ?";
在DB建立好表格後,全部的Java Configuration如下:
- 當設定好DataSource後,只要改用JdbcUserDetailsManager Bean,並將此Bean配置到DaoAuthenticationProvider即可。
- 註解掉的程式只是用來建立DB中的資料而已
- 密碼加密則只要配置BCryptPasswordEncoder Bean,再將此Bean設定到DaoAuthenticationProvider即可。
(*注意: Password存DB前需先加密才行,如下範例。)
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
DataSource myds;
@Bean
@Override
protected UserDetailsService userDetailsService(){
JdbcUserDetailsManager manager = new JdbcUserDetailsManager();
manager.setDataSource(myds);
//Only for initiating empty database
//PasswordEncoder encoder = passwordEncoder();
//manager.createUser(User.withUsername("admin")
.password(encoder.encode("123456")).authorities("ADMIN").build());
//manager.createUser(User.withUsername("user")
.password(encoder.encode("123456")).authorities("USER").build());
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() { //配置密碼加密元件
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder()); //在Auth物件上設定加密元件
return authProvider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/bill/**").authenticated()
.antMatchers("/product/**").permitAll()
.and()
.httpBasic();
}
}
5.8、
取得認證資料
Spring Security配置完成了,接下來看Controller中要如何取得認證相關的資料,如使用者ID、角色等。跟一般Spring在Controller中使用參數注入一樣,只要寫在Method參數即可取得,如下getBill():
@GetMapping("/product/{id}")
public String getProduct(@PathVariable String id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return "product id : " + id + " (Authenticated= " + authentication.getName() + ")";
}
@RequestMapping("/bill/{id}")
public String getBill(@PathVariable String id, Authentication authentication, Principal principal) {
return "bill id : " + id + " (Authenticated= " + authentication.getName() + ")";
}
若無法用注入的方式,只要如getProduct()一樣,直接引用SecurityContextHolder亦可取得認證相關的資料。
5.9、 測試
用5.7中的範例,URL “/bill/**”需要http basic認證才能通過,

輸入id/pwd後,取得結果如下,使用者為”user_3”

URL “/product/**”為permitAll可直接進入,使用者為匿名使用者。

5.10、 POM
最後我們看一下使用Spring Security的pom.xml,其基本相依配置如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.4.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MariaDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
*基本上只要spring-boot-starter-security即可,因為有將使用者存放在DB中,所以用到JPA與MariaDB。
5.11、 YAML配置檔
以下是用Spring Security最單純的設配置內容:
server:
port: 8080
spring:
datasource:
url: <db url>
username: <dbuser>
password: <dbpwd>
driver-class-name: <or driverClassName>
logging:
level:
ROOT: INFO
6. 參考來源
l Spring Security
https://projects.spring.io/spring-security/
l Getting Started Spring Security
https://www.codeproject.com/Articles/253901/Getting-Started-Spring-Security
l Hello Spring Security Java Config
https://docs.spring.io/spring-security/site/docs/current/guides/html5//helloworld-javaconfig.html
l Creating a Custom Login Form
https://docs.spring.io/spring-security/site/docs/current/guides/html5/form-javaconfig.html