一、Spring Security 簡介
所有的業務系統都需要鑒權、授權的步驟,通過鑒權,授權提高系統的安全性,只有合法的用戶才能對系統進行操作,外部系統通過鑒權后才能調用本系統的接口等。鑒權、授權的實現有很多種,常見的有apache shiro 以及今天我們介紹的Spring Security,它們都屬於安全框架,幫助業務系統實現鑒權、授權的功能,讓我們有更多的經歷實現業務功能。
Spring Security 核心是一組過濾器鏈,通過過濾器來驗證用戶是否登錄、是否有權限訪問后台接口,我們也可以通過自定義過濾器實現不同方式的登錄,比如通過手機號+驗證碼的方式。
二、簡單使用Spring Security
本篇文章的demo是基於SpringBoot 2.2.5+tkmybatis+themlefy+mysql開發,security的版本是5.2.2.RELEASE;
(一)搭建項目
項目是標准的maven項目結構,具體的目錄如下圖所示,新建啟動類SpringSecurityApplication及測試類HelloController,只有一個簡單的測試方法返回字符串"hello,world"
@SpringBootApplication
@EnableAsync
@ComponentScan(value = "com.tl.spring.security")
public class SpringSecurityApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(SpringSecurityApplication.class);
}
@Override
public void onStartup(ServletContext servletContext)
throws ServletException {
servletContext.getSessionCookieConfig().setName("SESSIONID");
}
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(SpringSecurityApplication.class);
springApplication.addListeners(new ApplicationPidFileWriter());
springApplication.run(args);
}
}
(二)引入依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
</parent>
<groupId>com.tl</groupId>
<artifactId>spring-security</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<url>http://www.xxx.com</url>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>
(三)使用默認的配置
使用spring默認配置,不新建配置文件application.yml
(四)運行程序
運行程序,在控制台上回打印出來,此次生成的密碼,如下圖所示:
通過瀏覽器訪問127.0.0.1:8080/hello
跳轉到登錄頁面(security5.2版本默認好像不再使用http basic 認證),如下圖所示,輸入默認用戶名user
及控制台打印的密碼,登錄成功后,可正常訪問后台接口,並返回字符串hello,world
(五)使用內存用戶登錄
新建配置類AuthConfiguration
繼承WebSecurityConfigurerAdapter
重寫configure
方法
@Configuration
@EnableWebSecurity
public class AuthConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/hello/admin").hasAnyRole("ROOT").anyRequest().permitAll()
.antMatchers("/hello").hasRole("USER").anyRequest().permitAll()
.and()
.csrf().disable().
formLogin().and().httpBasic().disable()
.sessionManagement().disable()
.cors()
.and()
.logout();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 設置攔截忽略文件夾,可以對靜態資源放行包括css,js等
web.ignoring().antMatchers("/static/**");
}
/**
* 新建兩個用戶root 和user 分別擁有"ROLE_ROOT", "ROLE_USER" 和"ROLE_USER" 角色
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(getPasswordEncoder())
.withUser("root")
.password(getPasswordEncoder().encode("root@123456"))
.roles("ROLE_ROOT", "ROLE_USER")
.and()
.withUser("user")
.password(getPasswordEncoder().encode("user@123456"))
.roles("ROLE_USER");
}
/**
* 加密方式 security 5.0以后必須要求使用加密方式對明文密碼進行加密
* @return
*/
@Bean
private BCryptPasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
或者使用重寫userDetailsService()
方法新建內存用戶
@Override
@Bean
protected UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("root").password(getPasswordEncoder().encode("root@123456")).roles("ROOT").build());
manager.createUser(User.withUsername("user").password(getPasswordEncoder().encode("user@123456")).roles("USER").build());
return manager;
}
使用user/user@123456登錄后,可正常訪問/hello
接口,但是訪問/hello/admin
接口時前台報403無權訪問錯誤,說明我們的配置已經生效。
(六)使用數據庫用戶登錄
在實際應用開發中,我們不會使用以上方式(把用戶信息放到內存中),用戶信息應該存儲在DB中,通過查詢DB獲取用戶信息,Security提供有相應的接口,因我們自己的業務系統一般使用自己設計的權限模型,所以經常使用的方案是實現UserDetailService
接口中的loadUserByUsername
方法,根據用戶名返回UserDetails
對象。在使用自定義的登錄邏輯實現登錄之前,我們先看下security登錄驗證的整體流程。
1 .登錄驗證流程
(1) .用戶通過前台頁面發起登錄請求后,請求會被UsernamePasswordAuthenticationFilter
攔截,執行attemptAuthentication
方法,構建token對象,具體構建的token對象的過程比較簡單,可自行查看源碼。token生成后並交由AuthenticationManager
的實現類ProviderManager
的authenticate
方法來處理
(2) 用戶名密碼登錄流程所使用DaoAuthenticationProvider
繼承自AbstractUserDetailsAuthenticationProvider
,並實現了抽象方法retrieveUser
,獲取userDetailService獲取用戶信息后,對用戶進行認證。
(3).認證成功后調用createSuccessAuthentication()
方法,並返回認證信息,在此方法中 ,它重新 new 了一個 UsernamePasswordAuthenticationToken
,因為到這里認證已經通過了,所以將 authorities 注入進去,並設置 authenticated 為 true,即已通過認證。
(4)到此為止認證信息會返回到UsernamePasswordAuthenticationFilter
中,在 UsernamePasswordAuthenticationFilter
的父類 AbstractAuthenticationProcessingFilter
的 doFilter()
中,會根據認證的成功或者失敗調用相應的 handler,此handler可通過
2. 自定義類
根據登錄流程,需要自定義類實現UserDetailsService
接口,重寫loadUserByUsername
實現從自己的數據庫查詢用戶信息邏輯
@Component
public class SecurityUserDetailsService implements UserDetailsService {
private Logger log = LoggerFactory.getLogger(SecurityUserDetailsService.class);
@Autowired
private UserMapper userMapper ;
/**
* 根據用戶名稱查詢用戶信息,並返回UserDetails對象
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//自定義實現查找用戶的方式,這里不是重點,不再貼具體實現代碼
User user = userMapper.getUserByName(username);
if (user == null) {
log.error("Can not find user by name: {}", username);
throw new UsernameNotFoundException("Can not find user by name:"+username);
}
//返回User 對象
return org.springframework.security.core.userdetails.User
.withUsername(username)
.password(user.getPassword())
.roles("USER","ROOT").accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(false)
.build();
}
}
3. 修改配置
修改上面AuthConfiguration
的配置類,配置自定義的UserDetailsService
類及密碼加密方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityUserDetailsService).
passwordEncoder(getPasswordEncoder());
}
4. 自定義加密
通過實現PasswordEncoder
接口中的兩個方法,實現自定義的加密方式
//加密
String encode(CharSequence rawPassword);
//判斷密碼是否匹配
boolean matches(CharSequence rawPassword, String encodedPassword);