Spring Security(1)—Web 許可權方案
Spring Security(1)—Web 許可權方案
官方網址:https://spring.io/projects/spring-security
概要
Spring 是非常流行和成功的 Java 應用開發框架,Spring Security正是Spring家族中的成員。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。
Spring Security重要核心功能:使用者認證(Authentication)和使用者授權 (Authorization)。
(1)使用者認證指的是:驗證某個使用者是否為系統中的合法主體,也就是說使用者能否訪問該系統。使用者認證一般要求使用者提供使用者名稱和密碼。系統通過校驗使用者名稱和密碼來完成認證過程。通俗點說就是系統認為使用者是否能登入 。
(2)使用者授權指的是驗證某個使用者是否有許可權執行某個操作。在一個系統中,不同使用者 所具有的許可權是不同的。比如對一個檔案來說,有的使用者只能進行讀取,而有的使用者可以 進行修改。一般來說,系統會為不同的使用者分配不同的角色,而每個角色則對應一系列的 許可權。通俗點講就是系統判斷使用者是否有許可權去做某些事情。
與Shiro的區別
- Spring Security 優點:
1、Spring Security 基於Spring 開發,專案若使用 Spring 作為基礎,配合 Spring Security 做許可權更加方便,而 Shiro 需要和 Spring 進行整合開發;
2、Spring Security 功能比 Shiro 更加豐富些,例如安全維護方面;
3、Spring Security 社群資源相對比 Shiro 更加豐富;
- Spring Security 缺點:
4、Shiro 的配置和使用比較簡單,Spring Security 上手複雜些;
5、Shiro 依賴性低,不需要任何框架和容器,可以獨立執行.Spring Security 依賴Spring容器;
自從有了 Spring Boot 之後,Spring Boot 對於 Spring Security 提供了自動化配置方案,可以使用更少的配置來使用 Spring Security。 因此,一般來說,常見的安全管理技術棧的組合是這樣的:
-
SSM + Shiro
-
Spring Boot/Spring Cloud + Spring Security
UserDetailsService 介面
當什麼也沒有配置的時候,賬號和密碼是由Spring Security定義生成的。而在實際專案中 賬號和密碼都是從資料庫中查詢出來的。 所以我們要通過自定義邏輯控制認證邏輯。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
返回值 UserDetails ,這個類封裝了使用者”主體“。
// 表示獲取登入使用者所有許可權
Collection<? extends GrantedAuthority> getAuthorities();
// 表示獲取密碼
String getPassword();
// 表示獲取使用者名稱
String getUsername();
// 表示判斷賬戶是否過期
boolean isAccountNonExpired();
// 表示判斷賬戶是否被鎖定
boolean isAccountNonLocked();
// 表示憑證{密碼}是否過期
boolean isCredentialsNonExpired();
// 表示當前使用者是否可用
boolean isEnabled();
- UserDetailsService介面的實現類:
以後我們只需要使用 org.springframework.security.core.userdetails.Use
這個實體類即可!
PasswordEncoder 介面
- encode 方法
String encode(CharSequence rawPassword);
// 表示把引數按照特定的解析規則進行解析
- matches 方法
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示驗證從儲存中獲取的編碼密碼與編碼後提交的原始密碼是否匹配。
// 如果密碼匹配,則返回 true;如果不匹配,則返回 false。
//第一個引數表示需要被解析的密碼。第二個引數表示儲存的密碼。
- upgradeEncoding 方法
default boolean upgradeEncoding(String encodedPassword) { return false; }
// 表示如果解析的密碼能夠再次進行解析且達到更安全的結果則返回 true,否則返回 false。預設返回false。
- PasswordEncoder介面的實現類:
BCryptPasswordEncoder是Spring Security官方推薦的密碼解析器,平時可以使用這個解析器。
BCryptPasswordEncoder是對bcrypt強雜湊方法的具體實現。是基於Hash演算法實現的單向加密。可以通過strength控制加密強度,預設10。
SpringSecurity Web 許可權方案
配置認證策略
- 引入pom依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- 設定登入系統的賬號、密碼
- 方法一:在application.properties
spring.security.user.name=yinrz
spring.security.user.password=123
- 方法二:實現UserDetailsService介面,配置 SecurityConfig
@Service
public class LoginService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 可以實現資料庫認證來完成使用者登入,s引數為輸入的使用者名稱,根據使用者名稱查到實體物件,通過getUsername()、getPassword()獲取使用者名稱和密碼。
String username = "yinrz";
String password = passwordEncoder.encode("123"); // 密碼進行編碼
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(username,password,auths);
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("loginService")
private UserDetailsService userDetailsService;
// 選用BCryptPasswordEncoder密碼解析器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 配置使用自定義的 userDetailsService , 使用自定義的使用者名稱和密碼
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder()); //密碼進行解碼
}
}
- SecurityConfig 配置認證策略
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置認證策略
http.formLogin()
.loginPage("/login.html") // 配置登入頁面
.loginProcessingUrl("/login") // 配置登入的訪問路徑
.defaultSuccessUrl("/success.html") // 配置登入成功之後跳轉的 url,重定向
// .successForwardUrl("/success.html") 配置登入成功之後跳轉的 url,轉發
.permitAll();
http.authorizeRequests()
.antMatchers("/","/index").permitAll() // 符合這些路徑的,不需要認證
.anyRequest() // 其他請求
.authenticated(); // 需要認證
http.csrf().disable(); // 關閉 csrf
}
- 前端登入頁面
<form method="post" action="/login">
使用者名稱:<input type="text" name="username"/>
密碼:<input type="text" name="password"/>
<input type="submit" name="提交">
</form>
action 要與 loginProcessingUrl 一致,method 必須為 post,使用者名稱和密碼的 name 必須為 username 和 password。
原因:在執行登入的時候會走一個過濾器 UsernamePasswordAuthenticationFilter :
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
...
}
配置授權策略
- hasAuthority 方法
如果當前的主體具有指定的許可權,則返回 true,否則返回false 。
http.authorizeRequests()
.antMatchers("/update").hasAuthority("admin"); // 需要使用者帶有admin許可權
- hasAnyAuthority 方法
如果當前的主體有任何提供的角色(給定的作為一個逗號分隔的字串列表)的話,返回 true。
http.authorizeRequests()
.antMatchers("/update").hasAnyAuthority("admin,sale"); // 需要使用者帶有admin許可權或者sale許可權
- hasRole 方法
如果當前主體具有指定的角色,則返回true。
http.authorizeRequests()
.antMatchers("/update").hasRole("admin"); //需要使用者帶有admin角色
授予角色要加 “ROLE_” 字首:
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
- hasAnyRole 方法
表示使用者具備任何一個條件都可以訪問。
http.authorizeRequests()
.antMatchers("/update").hasRole("admin,sale"); //需要使用者帶有admin或者sale角色
- 自定義403沒有許可權頁面
http.exceptionHandling().accessDeniedPage("/unauth.html");
- 註解 @Secured
判斷是否具有角色,另外需要注意的是這裡匹配的字串需要新增字首“ROLE_“。
使用註解先要開啟註解功能 @EnableGlobalMethodSecurity(securedEnabled=true)
。
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityApplication.class, args);
}
}
@Secured({"ROLE_admin"})
@GetMapping("hello")
public String hello(){
return "Say Hello";
}
- 註解 @PreAuthorize
進入方法前進行許可權驗證。
使用註解先要開啟註解功能 @EnableGlobalMethodSecurity(prePostEnabled = true)
。
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SpringsecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityApplication.class, args);
}
}
@PreAuthorize("hasAnyAuthority('admin,sale')")
@GetMapping("hello")
public String hello(){
return "Say Hello";
}
- 註解 @PostAuthorize
在方法執行後再進行許可權驗證。
使用註解先要開啟註解功能 @EnableGlobalMethodSecurity(prePostEnabled = true)
。
@PostAuthorize("hasAnyAuthority('admin,sale')")
@GetMapping("hello")
public String hello(){
return "Say Hello";
}
- 註解 @PreFilter
進入控制器之前對資料進行過濾。
@PreFilter(value = "filterObject.id%2==0")
10.註解 @PostFilter
許可權驗證之後對資料進行過濾。
@PostFilter("filterObject.username == 'admin1'")
配置基於資料庫的記住我
- 建立表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 配置資料來源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
- 編寫配置類 SecurityConfig
@Autowired
@Qualifier("loginService")
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); // 賦值資料來源
jdbcTokenRepository.setDataSource(dataSource);
// jdbcTokenRepository.setCreateTableOnStartup(true); // 自動建立表
return jdbcTokenRepository;
}
http.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60*60*24*10) // 設定有效時長,單位秒
.userDetailsService(userDetailsService);
- 修改前端頁面
記住我:<input type="checkbox" name="remember-me" title="記住密碼"/>
name 必須為 remember-me 。
配置使用者登出
- 編寫配置類 SecurityConfig
http.logout()
.logoutUrl("/logout") // 登出的路徑
.logoutSuccessUrl("/index") // 登出成功後的跳轉的路徑
.permitAll();
- 修改前端頁面
<a href="/logout">退出</a>