Spring Security是Spring推出的一個(gè)安全框架碟渺,說(shuō)白了就是爭(zhēng)對(duì)用戶登錄和權(quán)限的框架,所以主要功能為兩塊:“認(rèn)證”和“授權(quán)”扫尖,對(duì)應(yīng)用戶登錄和是否有權(quán)限去訪問(wèn)一些功能
一、使用Spring Security
1. 依賴
SpringBoot項(xiàng)目中加入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 寫(xiě)一個(gè)測(cè)試接口
@RestController
public class DemoController {
@RequestMapping("/demo")
public String demo(){
return "demo";
}
}
訪問(wèn):http://localhost:8080/demo 后,跳轉(zhuǎn)的為:
默認(rèn)賬號(hào)為:user
衬浑,密碼在啟動(dòng)時(shí)的控制臺(tái)輸出:
輸入賬號(hào)密碼后登錄,就可以成功的訪問(wèn)接口了:
二放刨、自定義登錄邏輯
實(shí)際登錄中工秩,用戶的賬號(hào)密碼肯定需要通過(guò)數(shù)據(jù)庫(kù)查詢匹配,官方默認(rèn)只提供了一個(gè)默認(rèn)賬號(hào)进统,那么如何自定義用戶的登錄邏輯呢助币?
1. UserDetailsService
UserDetailsService
接口需要實(shí)現(xiàn)loadUserByUsername()
方法,該方法返回一個(gè)UserDetails
對(duì)象螟碎,該對(duì)象是一個(gè)接口眉菱,其方法對(duì)應(yīng)的解釋看下面的注解:
public interface UserDetails extends Serializable {
// 權(quán)限列表
Collection<? extends GrantedAuthority> getAuthorities();
// 密碼
String getPassword();
String getUsername();
// 賬號(hào)是否過(guò)期
boolean isAccountNonExpired();
// 賬號(hào)是否被鎖定
boolean isAccountNonLocked();
// 憑證是否過(guò)期
boolean isCredentialsNonExpired();
// 是否可以
boolean isEnabled();
}
其實(shí)現(xiàn)類為User
:
實(shí)現(xiàn)UserDetailsService
接口,并使用實(shí)現(xiàn)類User
構(gòu)造UserDetails
掉分,User
構(gòu)造傳入三個(gè)參數(shù):用戶名俭缓、密碼克伊、權(quán)限列表,密碼需要通過(guò)PasswordEncoder
將原密碼進(jìn)行編碼后傳入华坦,會(huì)自動(dòng)和前端傳入的密碼進(jìn)行匹配愿吹,權(quán)限暫時(shí)構(gòu)造空的即可:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模擬數(shù)據(jù)庫(kù)查詢操作
if (!username.equals("aruba")) throw new UsernameNotFoundException("用戶未找到");
// 查詢出的密碼
String pwd = "123";
// 密碼解析器
String encodePassword = passwordEncoder.encode(pwd);
User user = new User(username, encodePassword, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
return user;
}
}
2. PasswordEncoder
密碼解析器PasswordEncoder
接口的方法解釋:
public interface PasswordEncoder {
// 進(jìn)行編碼
String encode(CharSequence rawPassword);
// 原密碼和編碼后的密碼是否匹配
boolean matches(CharSequence rawPassword, String encodedPassword);
// 編碼的密碼能夠再次進(jìn)行解析且達(dá)到更安全的結(jié)果則返回true
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
密碼解析器實(shí)現(xiàn)類有很多:
我們使用最常用的BCryptPasswordEncoder
:
@Configuration
public class PasswordEncoderConfig {
/**
* 提供PasswordEncoder
*
* @return
*/
@Bean
protected PasswordEncoder providerPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
重啟項(xiàng)目,進(jìn)行訪問(wèn)季春,此時(shí)我們可以使用自定義的賬號(hào)和密碼進(jìn)行登錄了
三洗搂、自定義登錄界面
上面自定義了登錄邏輯,現(xiàn)在來(lái)對(duì)登錄界面進(jìn)行配置
1. 依賴
導(dǎo)入模板引擎:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. 頁(yè)面編寫(xiě)
SpringSecurity中默認(rèn)使用username
和password
作為登錄的請(qǐng)求參數(shù)载弄,默認(rèn)登錄接口:/login
耘拇,使用post
請(qǐng)求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用戶名:<input type="text" value="" name="username"><br/>
密碼: <input type="password" name="password"><br/>
<input type="submit" value="登錄">
</form>
</body>
</html>
3. SecurityFilterChain
提供注入SecurityFilterChain
的方法,該方法有一個(gè)入?yún)?code>HttpSecurity:
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/showLogin")//登錄頁(yè)面處理單元
.loginProcessingUrl("/login")//登錄時(shí)提交的請(qǐng)求
.successForwardUrl("/main");//登錄成功處理單元
http.authorizeRequests()
.antMatchers("/showLogin").permitAll() //配置不需要被認(rèn)證的請(qǐng)求
.anyRequest().authenticated();//其他請(qǐng)求都必須被認(rèn)證宇攻。必須登錄后才能訪問(wèn)惫叛。
http.csrf().disable();
return http.build();
}
}
4. controller編寫(xiě)
@Controller
public class LoginController {
@RequestMapping("/showLogin")
public String showLogin() {
return "login";
}
@RequestMapping("/main")
@ResponseBody
public String main() {
return "main";
}
}
訪問(wèn)效果:
5. handler
除了使用successForwardUrl
方法指定成功轉(zhuǎn)發(fā)的目標(biāo)外,還可以通過(guò)handler做自己想要的處理逞刷,比如使用重定向嘉涌,此處SpringSecurity不會(huì)做授權(quán)控制:
http.formLogin().loginPage("/showLogin")//登錄頁(yè)面處理單元
.loginProcessingUrl("/login")//登錄時(shí)提交的請(qǐng)求
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect("/main");
}
});
此外還有失敗的處理:failureHandler(AuthenticationFailureHandler)
方法,使用上是一樣的
四夸浅、URL匹配
1. antMatchers通配符
上面我們已經(jīng)使用過(guò)antMatchers()
方法來(lái)指定哪些請(qǐng)求不需要授權(quán)仑最,它還支持通配符
通配符 | 描述 |
---|---|
? | 匹配一個(gè)字符 |
* | 匹配0個(gè)或多個(gè)字符 |
** | 匹配0個(gè)或多個(gè)目錄 |
如放行js目錄下的所有文件:
.antMatchers("/js/**").permitAll()
2. antMatchers指定請(qǐng)求方式
通過(guò)第一個(gè)參數(shù),使用HttpMethod
指定請(qǐng)求方式:
.antMatchers(HttpMethod.GET,"/js/**").permitAll()
除了antMatchers()
方法以外帆喇,還可以使用regexMatchers()
方法警医,匹配規(guī)則為正則表達(dá)式
五、角色權(quán)限判斷
1. 設(shè)置請(qǐng)求的角色權(quán)限
Spring Security權(quán)限分為兩種:權(quán)限
和角色
坯钦,一個(gè)用戶可以擁有多個(gè)角色
预皇,而一個(gè)角色
可以擁有不同的權(quán)限
。
下面為配置權(quán)限的方法婉刀,在URL匹配后調(diào)用
權(quán)限方法 | 描述 |
---|---|
hasAuthority(String) |
只有擁有傳入?yún)?shù):權(quán)限 吟温,才允許訪問(wèn) |
hasAnyAuthority(String ...) |
擁有任意一個(gè)權(quán)限 ,都可以訪問(wèn) |
hasRole(String) |
只有具備傳入?yún)?shù):角色 突颊,才允許訪問(wèn) |
hasAnyRole(String ...) |
擁有任意一個(gè)角色 鲁豪,都可以訪問(wèn) |
hasIpAddress(String) |
指定ip,才可以訪問(wèn) |
示例:
.antMatchers("/register").hasAuthority("register")//只有有注冊(cè)權(quán)限
.antMatchers("/modify").hasAnyAuthority("modify","register")//任意一個(gè)權(quán)限
.antMatchers("/manage").hasRole("admin")//只有admin角色
.antMatchers("/show").hasAnyRole("admin","user")//任意一個(gè)角色
2. 分配用戶的角色權(quán)限
上面只是爭(zhēng)對(duì)不同的請(qǐng)求配置了權(quán)限和角色律秃,想要用戶擁有權(quán)限和角色呈昔,就需要在UserDetails
中進(jìn)行添加,之前我們權(quán)限暫時(shí)設(shè)置為了空友绝。
權(quán)限和角色設(shè)置規(guī)則:多個(gè)權(quán)限和角色使用,
分開(kāi),角色需要添加ROLE_
前綴:
User user = new User(username, encodePassword,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,modify"));
3. 拒絕權(quán)限處理
和successHandler()
一樣肝劲,Spring Security也可以自定義拒絕權(quán)限的處理迁客,使用accessDeniedHandler(AccessDeniedHandler)
方法:
// 拒絕訪問(wèn)的處理
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"權(quán)限不足郭宝,請(qǐng)聯(lián)系管理員!\"}");
out.flush();
out.close();
}
});
4. 權(quán)限自定義判斷
針對(duì)一些特殊的需求,我們可能要自定義權(quán)限的判斷邏輯掷漱,Spring Security也支持粘室,只要按照它提供的規(guī)則進(jìn)行代碼編寫(xiě)
4.1 boolean hasPermission(HttpServletRequest,Authentication)
需要定義一個(gè)接口,里面有該方法:
public interface MyAccessService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
實(shí)現(xiàn)接口:
@Service
public class MyAccessServiceImpl implements MyAccessService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 獲取對(duì)象
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
// 轉(zhuǎn)為UserDetails對(duì)象
UserDetails userDetails = (UserDetails) principal;
// 獲取所有權(quán)限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 做自己的處理
return authorities.contains(new SimpleGrantedAuthority("ROLE_admin"));
}
return false;
}
}
4.2 使用自定義
通過(guò)@+注入bean
的方式調(diào)用方法:
.anyRequest().access("@myAccessServiceImpl.hasPermission(request,authentication)");
5. 注解設(shè)置請(qǐng)求權(quán)限
除了通過(guò)config方式外卜范,還可以通過(guò)注解來(lái)指定controller層哪個(gè)請(qǐng)求使用哪些權(quán)限衔统,需要在SpringBoot啟動(dòng)類上開(kāi)啟@EnableMethodSecurity
注解:
支持的注解有:
注解 | 描述 |
---|---|
@Secured |
指定處理單元的權(quán)限和角色,參數(shù)為數(shù)組海雪,使用需要開(kāi)啟@EnableMethodSecurity 注解的securedEnabled
|
@PreAuthorize |
在處理單元之前進(jìn)行權(quán)限和角色的控制锦爵,使用權(quán)限表達(dá)式進(jìn)行授權(quán) |
@PostAuthorize |
在處理單元之后進(jìn)行權(quán)限和角色的控制 |
示例:
@RestController
public class DemoController {
// Secured角色需要加上ROLE前綴
@Secured({"ROLE_admin", "register"})
@RequestMapping("/demo")
public String demo() {
return "demo";
}
// PreAuthorize中可以調(diào)用方法
@PreAuthorize("hasRole('admin')")
@RequestMapping("/demo2")
public String demo2() {
return "demo";
}
}
六、記住登錄
Spring Security記住登錄功能依賴數(shù)據(jù)庫(kù)實(shí)現(xiàn)奥裸,需要進(jìn)行以下配置
1. 依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
yml中進(jìn)行配置:
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
2. PersistentTokenRepository
提供PersistentTokenRepository
险掀,使用它的實(shí)現(xiàn)類:JdbcTokenRepositoryImpl
@Configuration
public class RememberConfig {
@Autowired
private DataSource dataSource;
@Bean
protected PersistentTokenRepository providerTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 啟動(dòng)時(shí),自動(dòng)創(chuàng)建表湾宙,第二次啟動(dòng)注釋
jdbcTokenRepository.setCreateTableOnStartup(true);
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
}
3. 配置HttpSecurity
// 持久處理
http.rememberMe()
.userDetailsService(userDetailsService) //登錄邏輯交給哪個(gè)對(duì)象
.tokenValiditySeconds(5000) // 表示持久化時(shí)間
.tokenRepository(repository); //持久層對(duì)象
4. 頁(yè)面添加標(biāo)簽
添加name
為remember-me
的標(biāo)簽:
記住: <input type="checkbox" name="remember-me"><br/>
之后網(wǎng)頁(yè)只需要登錄一次樟氢,就可以持久5000s不需要登錄,即使服務(wù)重啟也可以保持登錄狀態(tài)