文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈為小伙伴奉上以下珍貴的學習資源:
- 瘋狂創客圈 經典圖書 : 《Netty Zookeeper Redis 高並發實戰》 面試必備 + 大廠必備 + 漲薪必備
- 瘋狂創客圈 經典圖書 : 《SpringCloud、Nginx高並發核心編程》 面試必備 + 大廠必備 + 漲薪必備
- 資源寶庫: Java程序員必備 網盤資源大集合 價值>1000元 隨便取 GO->【博客園總入口 】
- 獨孤九劍:Netty靈魂實驗 : 本地 100W連接 高並發實驗,瞬間提升Java內力
推薦2:史上最全 Java 面試題 21 個專題
史上最全 Java 面試題 21 個專題 | 阿里、京東、美團、頭條.... 隨意挑、橫着走!!! |
---|---|
Java基礎 | |
1: JVM面試題(史上最強、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14365820.html |
2:Java基礎面試題(史上最全、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14366081.html |
3:死鎖面試題(史上最強、持續更新) | [https://www.cnblogs.com/crazymakercircle/p/14323919.html] |
4:設計模式面試題 (史上最全、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14367101.html |
5:架構設計面試題 (史上最全、持續更新、吐血推薦) | https://www.cnblogs.com/crazymakercircle/p/14367907.html |
還有 10 + 篇必刷、必刷 的面試題 | 更多 ....., 請參見【 瘋狂創客圈 高並發 總目錄 】 |
推薦3: 瘋狂創客圈 高並發 高質量博文
springCloud 高質量 博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
springcloud + webflux 高並發實戰 | Webflux(史上最全) |
SpringCloud gateway (史上最全) | spring security (史上最全) |
還有 10 + 篇 必刷、必刷 的高質量 博文 | 更多 ....., 請參見【 瘋狂創客圈 高並發 總目錄 】 |
一、spring security 簡介
spring security 的核心功能主要包括:
- 認證 (你是誰)
- 授權 (你能干什么)
- 攻擊防護 (防止偽造身份)
其核心就是一組過濾器鏈,項目啟動后將會自動配置。最核心的就是 Basic Authentication Filter 用來認證用戶的身份,一個在spring security中一種過濾器處理一種認證方式。
比如,對於username password認證過濾器來說,
會檢查是否是一個登錄請求;
是否包含username 和 password (也就是該過濾器需要的一些認證信息) ;
如果不滿足則放行給下一個。
下一個按照自身職責判定是否是自身需要的信息,basic的特征就是在請求頭中有 Authorization:Basic eHh4Onh4 的信息。中間可能還有更多的認證過濾器。最后一環是 FilterSecurityInterceptor,這里會判定該請求是否能進行訪問rest服務,判斷的依據是 BrowserSecurityConfig中的配置,如果被拒絕了就會拋出不同的異常(根據具體的原因)。Exception Translation Filter 會捕獲拋出的錯誤,然后根據不同的認證方式進行信息的返回提示。
注意:綠色的過濾器可以配置是否生效,其他的都不能控制。
二、入門項目
首先創建spring boot項目HelloSecurity,其pom主要依賴如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
然后在src/main/resources/templates/目錄下創建頁面:
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
我們可以看到, 在這個簡單的視圖中包含了一個鏈接: “/hello”. 鏈接到了如下的頁面,Thymeleaf模板如下:
hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
Web應用程序基於Spring MVC。 因此,你需要配置Spring MVC並設置視圖控制器來暴露這些模板。 如下是一個典型的Spring MVC配置類。在src/main/java/hello目錄下(所以java都在這里):
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
addViewControllers()方法(覆蓋WebMvcConfigurerAdapter中同名的方法)添加了四個視圖控制器。 兩個視圖控制器引用名稱為“home”的視圖(在home.html中定義),另一個引用名為“hello”的視圖(在hello.html中定義)。 第四個視圖控制器引用另一個名為“login”的視圖。 將在下一部分中創建該視圖。此時,可以跳過來使應用程序可執行並運行應用程序,而無需登錄任何內容。然后啟動程序如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
2、加入Spring Security
假設你希望防止未經授權的用戶訪問“/ hello”。 此時,如果用戶點擊主頁上的鏈接,他們會看到問候語,請求被沒有被攔截。 你需要添加一個障礙,使得用戶在看到該頁面之前登錄。您可以通過在應用程序中配置Spring Security來實現。 如果Spring Security在類路徑上,則Spring Boot會使用“Basic認證”來自動保護所有HTTP端點。 同時,你可以進一步自定義安全設置。首先在pom文件中引入:
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
...
</dependencies>
如下是安全配置,使得只有認證過的用戶才可以訪問到問候頁面:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}
WebSecurityConfig類使用了@EnableWebSecurity注解 ,以啟用Spring Security的Web安全支持,並提供Spring MVC集成。它還擴展了WebSecurityConfigurerAdapter,並覆蓋了一些方法來設置Web安全配置的一些細節。
configure(HttpSecurity)方法定義了哪些URL路徑應該被保護,哪些不應該。具體來說,“/”和“/ home”路徑被配置為不需要任何身份驗證。所有其他路徑必須經過身份驗證。
當用戶成功登錄時,它們將被重定向到先前請求的需要身份認證的頁面。有一個由 loginPage()指定的自定義“/登錄”頁面,每個人都可以查看它。
對於configureGlobal(AuthenticationManagerBuilder) 方法,它將單個用戶設置在內存中。該用戶的用戶名為“user”,密碼為“password”,角色為“USER”。
現在我們需要創建登錄頁面。前面我們已經配置了“login”的視圖控制器,因此現在只需要創建登錄頁面即可:
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
你可以看到,這個Thymeleaf模板只是提供一個表單來獲取用戶名和密碼,並將它們提交到“/ login”。 根據配置,Spring Security提供了一個攔截該請求並驗證用戶的過濾器。 如果用戶未通過認證,該頁面將重定向到“/ login?error”,並在頁面顯示相應的錯誤消息。 注銷成功后,我們的應用程序將發送到“/ login?logout”,我們的頁面顯示相應的登出成功消息。最后,我們需要向用戶提供一個顯示當前用戶名和登出的方法。 更新hello.html 向當前用戶打印一句hello,並包含一個“注銷”表單,如下所示:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
三、參數詳解
1、注解 @EnableWebSecurity
在 Spring boot 應用中使用 Spring Security,用到了 @EnableWebSecurity注解,官方說明為,該注解和 @Configuration 注解一起使用, 注解 WebSecurityConfigurer 類型的類,或者利用@EnableWebSecurity 注解繼承 WebSecurityConfigurerAdapter的類,這樣就構成了 Spring Security 的配置。
2、抽象類 WebSecurityConfigurerAdapter
一般情況,會選擇繼承 WebSecurityConfigurerAdapter 類,其官方說明為:WebSecurityConfigurerAdapter 提供了一種便利的方式去創建 WebSecurityConfigurer的實例,只需要重寫 WebSecurityConfigurerAdapter 的方法,即可配置攔截什么URL、設置什么權限等安全控制。
3、方法 configure(AuthenticationManagerBuilder auth) 和 configure(HttpSecurity http)
Demo 中重寫了 WebSecurityConfigurerAdapter 的兩個方法:
/**
* 通過 {@link #authenticationManager()} 方法的默認實現嘗試獲取一個 {@link AuthenticationManager}.
* 如果被復寫, 應該使用{@link AuthenticationManagerBuilder} 來指定 {@link AuthenticationManager}.
*
* 例如, 可以使用以下配置在內存中進行注冊公開內存的身份驗證{@link UserDetailsService}:
*
* // 在內存中添加 user 和 admin 用戶
* @Override
* protected void configure(AuthenticationManagerBuilder auth) {
* auth
* .inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
* .withUser("admin").password("password").roles("USER", "ADMIN");
* }
*
* // 將 UserDetailsService 顯示為 Bean
* @Bean
* @Override
* public UserDetailsService userDetailsServiceBean() throws Exception {
* return super.userDetailsServiceBean();
* }
*
*/
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}
/**
* 復寫這個方法來配置 {@link HttpSecurity}.
* 通常,子類不能通過調用 super 來調用此方法,因為它可能會覆蓋其配置。 默認配置為:
*
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
*
*/
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
4、final 類 HttpSecurity
HttpSecurity 常用方法及說明:
方法 | 說明 |
---|---|
openidLogin() |
用於基於 OpenId 的驗證 |
headers() |
將安全標頭添加到響應 |
cors() |
配置跨域資源共享( CORS ) |
sessionManagement() |
允許配置會話管理 |
portMapper() |
允許配置一個PortMapper (HttpSecurity#(getSharedObject(class)) ),其他提供SecurityConfigurer 的對象使用 PortMapper 從 HTTP 重定向到 HTTPS 或者從 HTTPS 重定向到 HTTP。默認情況下,Spring Security使用一個PortMapperImpl 映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 |
jee() |
配置基於容器的預認證。 在這種情況下,認證由Servlet容器管理 |
x509() |
配置基於x509的認證 |
rememberMe |
允許配置“記住我”的驗證 |
authorizeRequests() |
允許基於使用HttpServletRequest 限制訪問 |
requestCache() |
允許配置請求緩存 |
exceptionHandling() |
允許配置錯誤處理 |
securityContext() |
在HttpServletRequests 之間的SecurityContextHolder 上設置SecurityContext 的管理。 當使用WebSecurityConfigurerAdapter 時,這將自動應用 |
servletApi() |
將HttpServletRequest 方法與在其上找到的值集成到SecurityContext 中。 當使用WebSecurityConfigurerAdapter 時,這將自動應用 |
csrf() |
添加 CSRF 支持,使用WebSecurityConfigurerAdapter 時,默認啟用 |
logout() |
添加退出登錄支持。當使用WebSecurityConfigurerAdapter 時,這將自動應用。默認情況是,訪問URL”/ logout”,使HTTP Session無效來清除用戶,清除已配置的任何#rememberMe() 身份驗證,清除SecurityContextHolder ,然后重定向到”/login?success” |
anonymous() |
允許配置匿名用戶的表示方法。 當與WebSecurityConfigurerAdapter 結合使用時,這將自動應用。 默認情況下,匿名用戶將使用org.springframework.security.authentication.AnonymousAuthenticationToken 表示,並包含角色 “ROLE_ANONYMOUS” |
formLogin() |
指定支持基於表單的身份驗證。如果未指定FormLoginConfigurer#loginPage(String) ,則將生成默認登錄頁面 |
oauth2Login() |
根據外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份驗證 |
requiresChannel() |
配置通道安全。為了使該配置有用,必須提供至少一個到所需信道的映射 |
httpBasic() |
配置 Http Basic 驗證 |
addFilterAt() |
在指定的Filter類的位置添加過濾器 |
5、類 AuthenticationManagerBuilder
/**
* {@link SecurityBuilder} used to create an {@link AuthenticationManager}. Allows for
* easily building in memory authentication, LDAP authentication, JDBC based
* authentication, adding {@link UserDetailsService}, and adding
* {@link AuthenticationProvider}'s.
*/
意思是,AuthenticationManagerBuilder 用於創建一個 AuthenticationManager,讓我能夠輕松的實現內存驗證、LADP驗證、基於JDBC的驗證、添加UserDetailsService、添加AuthenticationProvider。
使用yaml文件定義的用戶名、密碼登錄
在application.yaml中定義用戶名密碼:
spring:
security:
user:
name: root
password: root
使用root/root登錄,可以正常訪問/hello
。
使用代碼中指定的用戶名、密碼登錄
- 使用configure(AuthenticationManagerBuilder) 添加認證。
- 使用configure(httpSecurity) 添加權限
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin") // 添加用戶admin
.password("{noop}admin") // 不設置密碼加密
.roles("ADMIN", "USER")// 添加角色為admin,user
.and()
.withUser("user") // 添加用戶user
.password("{noop}user")
.roles("USER")
.and()
.withUser("tmp") // 添加用戶tmp
.password("{noop}tmp")
.roles(); // 沒有角色
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/product/**").hasRole("USER") //添加/product/** 下的所有請求只能由user角色才能訪問
.antMatchers("/admin/**").hasRole("ADMIN") //添加/admin/** 下的所有請求只能由admin角色才能訪問
.anyRequest().authenticated() // 沒有定義的請求,所有的角色都可以訪問(tmp也可以)。
.and()
.formLogin().and()
.httpBasic();
}
}
添加AdminController、ProductController
@RestController
@RequestMapping("/admin")
public class AdminController {
@RequestMapping("/hello")
public String hello(){
return "admin hello";
}
}
@RestController
@RequestMapping("/product")
public class ProductController {
@RequestMapping("/hello")
public String hello(){
return "product hello";
}
}
通過上面的設置,訪問http://localhost:8080/admin/hello只能由admin訪問,http://localhost:8080/product/hello admin和user都可以訪問,http://localhost:8080/hello 所有用戶(包括tmp)都可以訪問。
使用數據庫的用戶名、密碼登錄
添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
添加數據庫配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
配置spring-security認證和授權
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)// 設置自定義的userDetailsService
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/product/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() //
.and()
.formLogin()
.and()
.httpBasic()
.and().logout().logoutUrl("/logout");
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();// 使用不使用加密算法保持密碼
// return new BCryptPasswordEncoder();
}
}
如果需要使用BCryptPasswordEncoder
,可以先在測試環境中加密后放到數據庫中:
@Test
void encode() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("user");
String password2 = bCryptPasswordEncoder.encode("admin");
System.out.println(password);
System.out.println(password2);
}
配置自定義UserDetailsService來進行驗證
@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// 1. 查詢用戶
User userFromDatabase = userRepository.findOneByLogin(login);
if (userFromDatabase == null) {
//log.warn("User: {} not found", login);
throw new UsernameNotFoundException("User " + login + " was not found in db");
//這里找不到必須拋異常
}
// 2. 設置角色
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole());
grantedAuthorities.add(grantedAuthority);
return new org.springframework.security.core.userdetails.User(login,
userFromDatabase.getPassword(), grantedAuthorities);
}
}
配置JPA中的UserRepository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findOneByLogin(String login);
}
添加數據庫數據
CREATE TABLE `user` (
`id` int(28) NOT NULL,
`login` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`password` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`role` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (1, 'user', 'user', 'ROLE_USER');
INSERT INTO `demo`.`user`(`id`, `login`, `password`, `role`) VALUES (2, 'admin', 'admin', 'ROLE_ADMIN');
默認角色前綴必須是
ROLE_
,因為spring-security會在授權的時候自動使用match中的角色加上ROLE_
后進行比較。
四:獲取登錄信息
@RequestMapping("/info")
public String info(){
String userDetails = null;
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal instanceof UserDetails) {
userDetails = ((UserDetails)principal).getUsername();
}else {
userDetails = principal.toString();
}
return userDetails;
}
使用SecurityContextHolder.getContext().getAuthentication().getPrincipal();
獲取當前的登錄信息。
五: Spring Security 核心組件
SecurityContext
SecurityContext
是安全的上下文,所有的數據都是保存到SecurityContext中。
可以通過SecurityContext
獲取的對象有:
- Authentication
SecurityContextHolder
SecurityContextHolder
用來獲取SecurityContext中保存的數據的工具。通過使用靜態方法獲取SecurityContext的相對應的數據。
SecurityContext context = SecurityContextHolder.getContext();
Authentication
Authentication表示當前的認證情況,可以獲取的對象有:
UserDetails:獲取用戶信息,是否鎖定等額外信息。
Credentials:獲取密碼。
isAuthenticated:獲取是否已經認證過。
Principal:獲取用戶,如果沒有認證,那么就是用戶名,如果認證了,返回UserDetails。
UserDetails
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
UserDetailsService
UserDetailsService可以通過loadUserByUsername獲取UserDetails對象。該接口供spring security進行用戶驗證。
通常使用自定義一個CustomUserDetailsService來實現UserDetailsService接口,通過自定義查詢UserDetails。
AuthenticationManager
AuthenticationManager用來進行驗證,如果驗證失敗會拋出相對應的異常。
PasswordEncoder
密碼加密器。通常是自定義指定。
BCryptPasswordEncoder:哈希算法加密
NoOpPasswordEncoder:不使用加密
六:spring security session 無狀態支持權限控制(前后分離)
spring security會在默認的情況下將認證信息放到HttpSession中。
但是對於我們的前后端分離的情況,如app,小程序,web前后分離等,httpSession就沒有用武之地了。這時我們可以通過
configure(httpSecurity)
設置spring security是否使用httpSession。
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// code...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
//設置無狀態,所有的值如下所示。
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// code...
}
// code...
}
共有四種值,其中默認的是ifRequired。
- always – a session will always be created if one doesn’t already exist,沒有session就創建。
- ifRequired – a session will be created only if required (default),如果需要就創建(默認)。
- never – the framework will never create a session itself but it will use one if it already exists
- stateless – no session will be created or used by Spring Security 不創建不使用session
由於前后端不通過保存session和cookie來進行判斷,所以為了保證spring security能夠記錄登錄狀態,所以需要傳遞一個值,讓這個值能夠自我驗證來源,同時能夠得到數據信息。選型我們選擇JWT。對於java客戶端我們選擇使用jjwt。
添加依賴
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
創建工具類JWTProvider
JWTProvider需要至少提供兩個方法,一個用來創建我們的token,另一個根據token獲取Authentication。
provider需要保證Key密鑰是唯一的,使用init()構建,否則會拋出異常。
@Component
@Slf4j
public class JWTProvider {
private Key key; // 私鑰
private long tokenValidityInMilliseconds; // 有效時間
private long tokenValidityInMillisecondsForRememberMe; // 記住我有效時間
@Autowired
private JJWTProperties jjwtProperties; // jwt配置參數
@Autowired
private UserRepository userRepository;
@PostConstruct
public void init() {
byte[] keyBytes;
String secret = jjwtProperties.getSecret();
if (StringUtils.hasText(secret)) {
log.warn("Warning: the JWT key used is not Base64-encoded. " +
"We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security.");
keyBytes = secret.getBytes(StandardCharsets.UTF_8);
} else {
log.debug("Using a Base64-encoded JWT secret key");
keyBytes = Decoders.BASE64.decode(jjwtProperties.getBase64Secret());
}
this.key = Keys.hmacShaKeyFor(keyBytes); // 使用mac-sha算法的密鑰
this.tokenValidityInMilliseconds =
1000 * jjwtProperties.getTokenValidityInSeconds();
this.tokenValidityInMillisecondsForRememberMe =
1000 * jjwtProperties.getTokenValidityInSecondsForRememberMe();
}
public String createToken(Authentication authentication, boolean rememberMe) {
long now = (new Date()).getTime();
Date validity;
if (rememberMe) {
validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe);
} else {
validity = new Date(now + this.tokenValidityInMilliseconds);
}
User user = userRepository.findOneByLogin(authentication.getName());
Map<String ,Object> map = new HashMap<>();
map.put("sub",authentication.getName());
map.put("user",user);
return Jwts.builder()
.setClaims(map) // 添加body
.signWith(key, SignatureAlgorithm.HS512) // 指定摘要算法
.setExpiration(validity) // 設置有效時間
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token).getBody(); // 根據token獲取body
User principal;
Collection<? extends GrantedAuthority> authorities;
principal = userRepository.findOneByLogin(claims.getSubject());
authorities = principal.getAuthorities();
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
}
注意這里我們創建的User需要實現UserDetails對象,這樣我們可以根據
principal.getAuthorities()
獲取到權限,如果不實現UserDetails,那么需要自定義authorities並添加到UsernamePasswordAuthenticationToken中。
@Data
@Entity
@Table(name="user")
public class User implements UserDetails {
@Id
@Column
private Long id;
@Column
private String login;
@Column
private String password;
@Column
private String role;
@Override
// 獲取權限,這里就用簡單的方法
// 在spring security中,Authorities既可以是ROLE也可以是Authorities
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority(role));
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
創建登錄成功,登出成功處理器
登錄成功后向前台發送jwt。
認證成功,返回jwt:
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
PrintWriter writer = response.getWriter();
writer.println(jwtProvider.createToken(authentication, true));
}
}
登出成功:
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
void onLogoutSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3) throws IOException, ServletException{
PrintWriter writer = response.getWriter();
writer.println("logout success");
writer.flush();
}
}
設置登錄、登出、取消csrf防護
登出無法對token進行失效操作,可以使用數據庫保存token,然后在登出時刪除該token。
@Configuration
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// code...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// code...
// 添加登錄處理器
.formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {
PrintWriter writer = response.getWriter();
writer.println(jwtProvider.createToken(authentication, true));
})
// 取消csrf防護
.and().csrf().disable()
// code...
// 添加登出處理器
.and().logout().logoutUrl("/logout").logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
PrintWriter writer = response.getWriter();
writer.println("logout success");
writer.flush();
})
// code...
}
// code...
}
使用JWT集成spring-security
添加Filter供spring-security解析token,並向securityContext中添加我們的用戶信息。
在UsernamePasswordAuthenticationFilter.class之前我們需要執行根據token添加authentication。關鍵方法是從jwt中獲取authentication,然后添加到securityContext中。
在SecurityConfiguration中需要設置Filter添加的位置。
創建自定義Filter,用於jwt獲取authentication:
@Slf4j
public class JWTFilter extends GenericFilterBean {
private final static String HEADER_AUTH_NAME = "auth";
private JWTProvider jwtProvider;
public JWTFilter(JWTProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authToken = httpServletRequest.getHeader(HEADER_AUTH_NAME);
if (StringUtils.hasText(authToken)) {
// 從自定義tokenProvider中解析用戶
Authentication authentication = this.jwtProvider.getAuthentication(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 調用后續的Filter,如果上面的代碼邏輯未能復原“session”,SecurityContext中沒有想過信息,后面的流程會檢測出"需要登錄"
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
向HttpSecurity添加Filter和設置Filter位置:
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
// code...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
//設置添加Filter和位置
.and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
// code...
}
// code...
}
MySecurityConfiguration代碼
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JWTProvider jwtProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)// 設置自定義的userDetailsService
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//設置無狀態
.and()
.authorizeRequests() // 配置請求權限
.antMatchers("/product/**").hasRole("USER") // 需要角色
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // 所有的請求都需要登錄
.and()
// 配置登錄url,和登錄成功處理器
.formLogin().loginProcessingUrl("/login").successHandler((request, response, authentication) -> {
PrintWriter writer = response.getWriter();
writer.println(jwtProvider.createToken(authentication, true));
})
// 取消csrf防護
.and().csrf().disable()
.httpBasic()
// 配置登出url,和登出成功處理器
.and().logout().logoutUrl("/logout")
.logoutSuccessHandler((HttpServletRequest request, HttpServletResponse response, Authentication authentication) -> {
PrintWriter writer = response.getWriter();
writer.println("logout success");
writer.flush();
})
// 在UsernamePasswordAuthenticationFilter之前執行我們添加的JWTFilter
.and().addFilterBefore(new JWTFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
public void configure(WebSecurity web) {
// 添加不做權限的URL
web.ignoring()
.antMatchers("/swagger-resources/**")
.antMatchers("/swagger-ui.html")
.antMatchers("/webjars/**")
.antMatchers("/v2/**")
.antMatchers("/h2-console/**");
}
}
使用注解對方法進行權限管理
需要在
MySecurityConfiguration
上添加@EnableGlobalMethodSecurity(prePostEnabled = true)
注解,prePostEnabled默認為false,需要設置為true后才能全局的注解權限控制。
prePostEnabled設置為true后,可以使用四個注解:
添加實體類School:
@Data
public class School implements Serializable {
private Long id;
private String name;
private String address;
}
-
@PreAuthorize
在訪問之前就進行權限判斷
@RestController public class AnnoController { @Autowired private JWTProvider jwtProvider; @RequestMapping("/annotation") // @PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasAuthority('ROLE_ADMIN')") public String info(){ return "擁有admin權限"; } }
hasRole和hasAuthority都會對UserDetails中的getAuthorities進行判斷區別是hasRole會對字段加上
ROLE_
后再進行判斷,上例中使用了hasRole('ADMIN')
,那么就會使用ROLE_ADMIN
進行判斷,如果是hasAuthority('ADMIN')
,那么就使用ADMIN
進行判斷。 -
@PostAuthorize
在請求之后進行判斷,如果返回值不滿足條件,會拋出異常,但是方法本身是已經執行過了的。
@RequestMapping("/postAuthorize") @PreAuthorize("hasRole('ADMIN')") @PostAuthorize("returnObject.id%2==0") public School postAuthorize(Long id) { School school = new School(); school.setId(id); return school; }
returnObject是內置對象,引用的是方法的返回值。
如果
returnObject.id%2==0
為 true,那么返回方法值。如果為false,會返回403 Forbidden。 -
@PreFilter
在方法執行之前,用於過濾集合中的值。
@RequestMapping("/preFilter") @PreAuthorize("hasRole('ADMIN')") @PreFilter("filterObject%2==0") public List<Long> preFilter(@RequestParam("ids") List<Long> ids) { return ids; }
filterObject
是內置對象,引用的是集合中的泛型類,如果有多個集合,需要指定filterTarget
。@PreFilter(filterTarget="ids", value="filterObject%2==0") public List<Long> preFilter(@RequestParam("ids") List<Long> ids,@RequestParam("ids") List<User> users,) { return ids; }
filterObject%2==0
會對集合中的值會進行過濾,為true的值會保留。第一個例子返回的值在執行前過濾返回2,4。
-
@PostFilter
會對返回的集合進行過濾。
@RequestMapping("/postFilter") @PreAuthorize("hasRole('ADMIN')") @PostFilter("filterObject.id%2==0") public List<School> postFilter() { List<School> schools = new ArrayList<School>(); School school; for (int i = 0; i < 10; i++) { school = new School(); school.setId((long)i); schools.add(school); } return schools; }
上面的方法返回結果為:id為0,2,4,6,8的School對象。
七、原理講解
1、校驗流程圖
2、源碼分析
- AbstractAuthenticationProcessingFilter 抽象類
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
調用 requiresAuthentication(HttpServletRequest, HttpServletResponse) 決定是否需要進行驗證操作。如果需要驗證,則會調用 attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法,有三種結果:
- 返回一個 Authentication 對象。配置的 SessionAuthenticationStrategy` 將被調用,然后 然后調用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
- 驗證時發生 AuthenticationException。unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法將被調用。
- 返回Null,表示身份驗證不完整。假設子類做了一些必要的工作(如重定向)來繼續處理驗證,方法將立即返回。假設后一個請求將被這種方法接收,其中返回的Authentication對象不為空。
- UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子類)
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
attemptAuthentication () 方法將 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 對象,用於 AuthenticationManager 的驗證(即 this.getAuthenticationManager().authenticate(authRequest) )。默認情況下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。
- ProviderManager(AuthenticationManager的實現類)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
Authentication result = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
eventPublisher.publishAuthenticationSuccess(result);
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
prepareException(lastException, authentication);
throw lastException;
}
嘗試驗證 Authentication 對象。AuthenticationProvider 列表將被連續嘗試,直到 AuthenticationProvider 表示它能夠認證傳遞的過來的Authentication 對象。然后將使用該 AuthenticationProvider 嘗試身份驗證。如果有多個 AuthenticationProvider 支持驗證傳遞過來的Authentication 對象,那么由第一個來確定結果,覆蓋早期支持AuthenticationProviders 所引發的任何可能的AuthenticationException。 成功驗證后,將不會嘗試后續的AuthenticationProvider。如果最后所有的 AuthenticationProviders 都沒有成功驗證 Authentication 對象,將拋出 AuthenticationException。從代碼中不難看出,由 provider 來驗證 authentication, 核心點方法是:
Authentication result = provider.authenticate(authentication);
此處的 provider 是 AbstractUserDetailsAuthenticationProvider,AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的實現,看看它的 authenticate(authentication) 方法:
// 驗證 authentication
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
AbstractUserDetailsAuthenticationProvider 內置了緩存機制,從緩存中獲取不到的 UserDetails 信息的話,就調用如下方法獲取用戶信息,然后和 用戶傳來的信息進行對比來判斷是否驗證成功。
// 獲取用戶信息
UserDetails user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
retrieveUser() 方法在 DaoAuthenticationProvider 中實現,DaoAuthenticationProvider 是 AbstractUserDetailsAuthenticationProvider的子類。具體實現如下:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
UserDetails loadedUser;
try {
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
}
catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
passwordEncoder.isPasswordValid(userNotFoundEncodedPassword,
presentedPassword, null);
}
throw notFound;
}
catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
可以看到此處的返回對象 userDetails 是由 UserDetailsService 的 #loadUserByUsername(username) 來獲取的。
八、玩轉自定義登錄
1. form 登錄的流程
下面是 form 登錄的基本流程:
只要是 form 登錄基本都能轉化為上面的流程。接下來我們看看 Spring Security 是如何處理的。
3. Spring Security 中的登錄
默認它提供了三種登錄方式:
formLogin()
普通表單登錄oauth2Login()
基於OAuth2.0
認證/授權協議openidLogin()
基於OpenID
身份認證規范
以上三種方式統統是 AbstractAuthenticationFilterConfigurer
實現的,
4. HttpSecurity 中的 form 表單登錄
啟用表單登錄通過兩種方式一種是通過 HttpSecurity
的 apply(C configurer)
方法自己構造一個 AbstractAuthenticationFilterConfigurer
的實現,這種是比較高級的玩法。 另一種是我們常見的使用 HttpSecurity
的 formLogin()
方法來自定義 FormLoginConfigurer
。我們先搞一下比較常規的第二種。
4.1 FormLoginConfigurer
該類是 form 表單登錄的配置類。它提供了一些我們常用的配置方法:
- loginPage(String loginPage) : 登錄 頁面而並不是接口,對於前后分離模式需要我們進行改造 默認為
/login
。 - loginProcessingUrl(String loginProcessingUrl) 實際表單向后台提交用戶信息的
Action
,再由過濾器UsernamePasswordAuthenticationFilter
攔截處理,該Action
其實不會處理任何邏輯。 - usernameParameter(String usernameParameter) 用來自定義用戶參數名,默認
username
。 - passwordParameter(String passwordParameter) 用來自定義用戶密碼名,默認
password
- failureUrl(String authenticationFailureUrl) 登錄失敗后會重定向到此路徑, 一般前后分離不會使用它。
- failureForwardUrl(String forwardUrl) 登錄失敗會轉發到此, 一般前后分離用到它。 可定義一個
Controller
(控制器)來處理返回值,但是要注意RequestMethod
。 - defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默認登陸成功后跳轉到此 ,如果
alwaysUse
為true
只要進行認證流程而且成功,會一直跳轉到此。一般推薦默認值false
- successForwardUrl(String forwardUrl) 效果等同於上面
defaultSuccessUrl
的alwaysUse
為true
但是要注意RequestMethod
。 - successHandler(AuthenticationSuccessHandler successHandler) 自定義認證成功處理器,可替代上面所有的
success
方式 - failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定義失敗成功處理器,可替代上面所有的
failure
方式 - permitAll(boolean permitAll) form 表單登錄是否放開
知道了這些我們就能來搞個定制化的登錄了。
5. Spring Security 聚合登錄 實戰
接下來是我們最激動人心的實戰登錄操作。 有疑問的可認真閱讀 Spring 實戰 的一系列預熱文章。
5.1 簡單需求
我們的接口訪問都要通過認證,登陸錯誤后返回錯誤信息(json),成功后前台可以獲取到對應數據庫用戶信息(json)(實戰中記得脫敏)。
我們定義處理成功失敗的控制器:
@RestController
@RequestMapping("/login")
public class LoginController {
@Resource
private SysUserService sysUserService;
/**
* 登錄失敗返回 401 以及提示信息.
*
* @return the rest
*/
@PostMapping("/failure")
public Rest loginFailure() {
return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登錄失敗了,老哥");
}
/**
* 登錄成功后拿到個人信息.
*
* @return the rest
*/
@PostMapping("/success")
public Rest loginSuccess() {
// 登錄成功后用戶的認證信息 UserDetails會存在 安全上下文寄存器 SecurityContextHolder 中
User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = principal.getUsername();
SysUser sysUser = sysUserService.queryByUsername(username);
// 脫敏
sysUser.setEncodePassword("[PROTECT]");
return RestBody.okData(sysUser,"登錄成功");
}
}
然后 我們自定義配置覆寫 void configure(HttpSecurity http)
方法進行如下配置(這里需要禁用crsf):
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.cors()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.loginProcessingUrl("/process")
.successForwardUrl("/login/success").
failureForwardUrl("/login/failure");
}
}
}
使用 Postman 或者其它工具進行 Post 方式的表單提交 http://localhost:8080/process?username=Felordcn&password=12345
會返回用戶信息:
{
"httpStatus": 200,
"data": {
"userId": 1,
"username": "Felordcn",
"encodePassword": "[PROTECT]",
"age": 18
},
"msg": "登錄成功",
"identifier": ""
}
把密碼修改為其它值再次請求認證失敗后 :
{
"httpStatus": 401,
"data": null,
"msg": "登錄失敗了,老哥",
"identifier": "-9999"
}
6. 多種登錄方式的簡單實現
就這么完了了么?現在登錄的花樣繁多。常規的就有短信、郵箱、掃碼 ,第三方是以后我要講的不在今天范圍之內。 如何應對想法多的產品經理? 我們來搞一個可擴展各種姿勢的登錄方式。我們在上面 2. form 登錄的流程 中的 用戶 和 判定 之間增加一個適配器來適配即可。 我們知道這個所謂的 判定就是 UsernamePasswordAuthenticationFilter
。
我們只需要保證 uri 為上面配置的/process 並且能夠通過 getParameter(String name) 獲取用戶名和密碼即可 。
我突然覺得可以模仿 DelegatingPasswordEncoder
的搞法, 維護一個注冊表執行不同的處理策略。當然我們要實現一個 GenericFilterBean
在 UsernamePasswordAuthenticationFilter
之前執行。同時制定登錄的策略。
6.1 登錄方式定義
定義登錄方式枚舉 ``。
public enum LoginTypeEnum {
/**
* 原始登錄方式.
*/
FORM,
/**
* Json 提交.
*/
JSON,
/**
* 驗證碼.
*/
CAPTCHA
}
6.2 定義前置處理器接口
定義前置處理器接口用來處理接收的各種特色的登錄參數 並處理具體的邏輯。這個借口其實有點隨意 ,重要的是你要學會思路。我實現了一個 默認的 form' 表單登錄 和 通過
RequestBody放入
json` 的兩種方式,篇幅限制這里就不展示了。具體的 DEMO 參見底部。
public interface LoginPostProcessor {
/**
* 獲取 登錄類型
*
* @return the type
*/
LoginTypeEnum getLoginTypeEnum();
/**
* 獲取用戶名
*
* @param request the request
* @return the string
*/
String obtainUsername(ServletRequest request);
/**
* 獲取密碼
*
* @param request the request
* @return the string
*/
String obtainPassword(ServletRequest request);
}
6.3 實現登錄前置處理過濾器
該過濾器維護了 LoginPostProcessor
映射表。 通過前端來判定登錄方式進行策略上的預處理,最終還是會交給 UsernamePasswordAuthenticationFilter
。通過 HttpSecurity
的 addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
方法進行前置。
package cn.felord.spring.security.filter;
import cn.felord.spring.security.enumation.LoginTypeEnum;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;
/**
* 預登錄控制器
*
* @author Felordcn
* @since 16 :21 2019/10/17
*/
public class PreLoginFilter extends GenericFilterBean {
private static final String LOGIN_TYPE_KEY = "login_type";
private RequestMatcher requiresAuthenticationRequestMatcher;
private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();
public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);
if (!CollectionUtils.isEmpty(loginPostProcessors)) {
loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
}
}
private LoginTypeEnum getTypeFromReq(ServletRequest request) {
String parameter = request.getParameter(LOGIN_TYPE_KEY);
int i = Integer.parseInt(parameter);
LoginTypeEnum[] values = LoginTypeEnum.values();
return values[i];
}
/**
* 默認還是Form .
*
* @return the login post processor
*/
private LoginPostProcessor defaultLoginPostProcessor() {
return new LoginPostProcessor() {
@Override
public LoginTypeEnum getLoginTypeEnum() {
return LoginTypeEnum.FORM;
}
@Override
public String obtainUsername(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
}
@Override
public String obtainPassword(ServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
}
};
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
LoginTypeEnum typeFromReq = getTypeFromReq(request);
LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);
String username = loginPostProcessor.obtainUsername(request);
String password = loginPostProcessor.obtainPassword(request);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
}
chain.doFilter(parameterRequestWrapper, response);
}
}
6.4 驗證
通過 POST
表單提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0
可以請求成功。或者以下列方式也可以提交成功:
更多的方式 只需要實現接口 LoginPostProcessor
注入 PreLoginFilter
九 整合JWT做登錄認證
JWT是JSON Web Token的縮寫,是目前最流行的跨域認證解決方法。
互聯網服務認證的一般流程是:
- 用戶向服務器發送賬號、密碼
- 服務器驗證通過后,將用戶的角色、登錄時間等信息保存到當前會話中
- 同時,服務器向用戶返回一個session_id(一般保存在cookie里)
- 用戶再次發送請求時,把含有session_id的cookie發送給服務器
- 服務器收到session_id,查找session,提取用戶信息
上面的認證模式,存在以下缺點:
- cookie不允許跨域
- 因為每台服務器都必須保存session對象,所以擴展性不好
JWT認證原理是:
- 用戶向服務器發送賬號、密碼
- 服務器驗證通過后,生成token令牌返回給客戶端(token可以包含用戶信息)
- 用戶再次請求時,把token放到請求頭
Authorization
里 - 服務器收到請求,驗證token合法后放行請求
JWT token令牌可以包含用戶身份、登錄時間等信息,這樣登錄狀態保持者由服務器端變為客戶端,服務器變成無狀態了;token放到請求頭,實現了跨域
JWT的組成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT由三部分組成:
- Header(頭部)
- Payload(負載)
- Signature(簽名)
表現形式為:Header.Payload.Signature
Header 部分是一個 JSON 對象,描述 JWT 的元數據,通常是下面的樣子:
{
"alg": "HS256",
"typ": "JWT"
}
上面代碼中,alg
屬性表示簽名的算法(algorithm),默認是 HMAC SHA256(寫成 HS256);typ
屬性表示這個令牌(token)的類型(type),JWT 令牌統一寫為JWT
。
上面的 JSON 對象使用 Base64URL 算法轉成字符串
Payload
Payload 部分也是一個 JSON 對象,用來存放實際需要傳遞的數據。JWT 規定了7個官方字段:
- iss (issuer):簽發人
- exp (expiration time):過期時間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號
當然,用戶也可以定義私有字段。
這個 JSON 對象也要使用 Base64URL 算法轉成字符串
Signature
Signature 部分是對前兩部分的簽名,防止數據篡改
簽名算法如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"."分隔
JWT認證和授權
Security是基於AOP和Servlet過濾器的安全框架,為了實現JWT要重寫那些方法、自定義那些過濾器需要首先了解security自帶的過濾器。security默認過濾器鏈如下:
- org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
- org.springframework.security.web.context.SecurityContextPersistenceFilter
- org.springframework.security.web.header.HeaderWriterFilter
- org.springframework.security.web.authentication.logout.LogoutFilter
- org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
- org.springframework.security.web.savedrequest.RequestCacheAwareFilter
- org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
- org.springframework.security.web.authentication.AnonymousAuthenticationFilter
- org.springframework.security.web.session.SessionManagementFilter
- org.springframework.security.web.access.ExceptionTranslationFilter
- org.springframework.security.web.access.intercept.FilterSecurityInterceptor
SecurityContextPersistenceFilter
這個過濾器有兩個作用:
- 用戶發送請求時,從session對象提取用戶信息,保存到SecurityContextHolder的securitycontext中
- 當前請求響應結束時,把SecurityContextHolder的securitycontext保存的用戶信息放到session,便於下次請求時共享數據;同時將SecurityContextHolder的securitycontext清空
由於禁用session功能,所以該過濾器只剩一個作用即把SecurityContextHolder的securitycontext清空。舉例來說明為何要清空securitycontext:用戶1發送一個請求,由線程M處理,當響應完成線程M放回線程池;用戶2發送一個請求,本次請求同樣由線程M處理,由於securitycontext沒有清空,理應儲存用戶2的信息但此時儲存的是用戶1的信息,造成用戶信息不符
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter
繼承自AbstractAuthenticationProcessingFilter
,處理邏輯在doFilter
方法中:
- 當請求被
UsernamePasswordAuthenticationFilter
攔截時,判斷請求路徑是否匹配登錄URL,若不匹配繼續執行下個過濾器;否則,執行步驟2 - 調用
attemptAuthentication
方法進行認證。UsernamePasswordAuthenticationFilter
重寫了attemptAuthentication
方法,負責讀取表單登錄參數,委托AuthenticationManager
進行認證,返回一個認證過的token(null表示認證失敗) - 判斷token是否為null,非null表示認證成功,null表示認證失敗
- 若認證成功,調用
successfulAuthentication
。該方法把認證過的token放入securitycontext供后續請求授權,同時該方法預留一個擴展點(AuthenticationSuccessHandler.onAuthenticationSuccess方法
),進行認證成功后的處理 - 若認證失敗,同樣可以擴展
uthenticationFailureHandler.onAuthenticationFailure
進行認證失敗后的處理 - 只要當前請求路徑匹配登錄URL,那么無論認證成功還是失敗,當前請求都會響應完成,不再執行過濾器鏈
UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法,執行邏輯如下:
- 從請求中獲取表單參數。因為使用
HttpServletRequest.getParameter
方法獲取參數,它只能處理Content-Type為application/x-www-form-urlencoded或multipart/form-data的請求,若是application/json則無法獲取值 - 把步驟1獲取的賬號、密碼封裝成
UsernamePasswordAuthenticationToken
對象,創建未認證的token。UsernamePasswordAuthenticationToken
有兩個重載的構造方法,其中public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
創建未經認證的token,public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
創建已認證的token - 獲取認證管理器
AuthenticationManager
,其缺省實現為ProviderManager
,調用其authenticate
進行認證 ProviderManager
的authenticate
是個模板方法,它遍歷所有AuthenticationProvider
,直至找到支持認證某類型token的AuthenticationProvider
,調用AuthenticationProvider.authenticate
方法認證,AuthenticationProvider.authenticate
加載正確的賬號、密碼進行比較驗證AuthenticationManager.authenticate
方法返回一個已認證的token
AnonymousAuthenticationFilter
AnonymousAuthenticationFilter
負責創建匿名token:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(this.createAuthentication((HttpServletRequest)req));
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Set SecurityContextHolder to " + SecurityContextHolder.getContext().getAuthentication();
}));
} else {
this.logger.debug("Set SecurityContextHolder to anonymous SecurityContext");
}
} else if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.of(() -> {
return "Did not set SecurityContextHolder since already authenticated " + SecurityContextHolder.getContext().getAuthentication();
}));
}
chain.doFilter(req, res);
}
如果當前用戶沒有認證,會創建一個匿名token,用戶是否能讀取資源交由FilterSecurityInterceptor
過濾器委托給決策管理器判斷是否有權限讀取
實現思路
JWT認證思路:
- 利用Security原生的表單認證過濾器驗證用戶名、密碼
- 驗證通過后自定義
AuthenticationSuccessHandler
認證成功處理器,由該處理器生成token令牌
JWT授權思路:
- 使用JWT目的是讓服務器變成無狀態,不用session共享數據,所以要禁用security的session功能(http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS))
- token令牌數據結構設計時,payload部分要儲存用戶名、角色信息
- token令牌有兩個作用:
- 認證, 用戶發送的token合法即代表認證成功
- 授權,令牌驗證成功后提取角色信息,構造認證過的token,將其放到securitycontext,具體權限判斷交給security框架處理
- 自己實現一個過濾器,攔截用戶請求,實現(3)中所說的功能
代碼實現 創建JWT工具類
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
我們對java-jwt提供的API進行封裝,便於創建、驗證、提取claim
@Slf4j
public class JWTUtil {
// 攜帶token的請求頭名字
public final static String TOKEN_HEADER = "Authorization";
//token的前綴
public final static String TOKEN_PREFIX = "Bearer ";
// 默認密鑰
public final static String DEFAULT_SECRET = "mySecret";
// 用戶身份
private final static String ROLES_CLAIM = "roles";
// token有效期,單位分鍾;
private final static long EXPIRE_TIME = 5 * 60 * 1000;
// 設置Remember-me功能后的token有效期
private final static long EXPIRE_TIME_REMEMBER = 7 * 24 * 60 * 60 * 1000;
// 創建token
public static String createToken(String username, List role, String secret, boolean rememberMe) {
Date expireDate = rememberMe ? new Date(System.currentTimeMillis() + EXPIRE_TIME_REMEMBER) : new Date(System.currentTimeMillis() + EXPIRE_TIME);
try {
// 創建簽名的算法實例
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withExpiresAt(expireDate)
.withClaim("username", username)
.withClaim(ROLES_CLAIM, role)
.sign(algorithm);
return token;
} catch (JWTCreationException jwtCreationException) {
log.warn("Token create failed");
return null;
}
}
// 驗證token
public static boolean verifyToken(String token, String secret) {
try{
Algorithm algorithm = Algorithm.HMAC256(secret);
// 構建JWT驗證器,token合法同時pyload必須含有私有字段username且值一致
// token過期也會驗證失敗
JWTVerifier verifier = JWT.require(algorithm)
.build();
// 驗證token
DecodedJWT decodedJWT = verifier.verify(token);
return true;
} catch (JWTVerificationException jwtVerificationException) {
log.warn("token驗證失敗");
return false;
}
}
// 獲取username
public static String getUsername(String token) {
try {
// 因此獲取載荷信息不需要密鑰
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException jwtDecodeException) {
log.warn("提取用戶姓名時,token解碼失敗");
return null;
}
}
public static List<String> getRole(String token) {
try {
// 因此獲取載荷信息不需要密鑰
DecodedJWT jwt = JWT.decode(token);
// asList方法需要指定容器元素的類型
return jwt.getClaim(ROLES_CLAIM).asList(String.class);
} catch (JWTDecodeException jwtDecodeException) {
log.warn("提取身份時,token解碼失敗");
return null;
}
}
}
代碼實現認證
驗證賬號、密碼交給UsernamePasswordAuthenticationFilter
,不用修改代碼
認證成功后,需要生成token返回給客戶端,我們通過擴展AuthenticationSuccessHandler.onAuthenticationSuccess方法
實現
@Component
public class JWTAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setCode("200");
responseData.setMessage("登錄成功!");
// 提取用戶名,准備寫入token
String username = authentication.getName();
// 提取角色,轉為List<String>對象,寫入token
List<String> roles = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities){
roles.add(authority.getAuthority());
}
// 創建token
String token = JWTUtil.createToken(username, roles, JWTUtil.DEFAULT_SECRET, true);
httpServletResponse.setCharacterEncoding("utf-8");
// 為了跨域,把token放到響應頭WWW-Authenticate里
httpServletResponse.setHeader("WWW-Authenticate", JWTUtil.TOKEN_PREFIX + token);
// 寫入響應里
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getWriter(), responseData);
}
}
為了統一返回值,我們封裝了一個ResponseData
對象
代碼實現 授權
自定義一個過濾器JWTAuthorizationFilter
,驗證token,token驗證成功后認為認證成功
@Slf4j
public class JWTAuthorizationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = getTokenFromRequestHeader(request);
Authentication verifyResult = verefyToken(token, JWTUtil.DEFAULT_SECRET);
if (verifyResult == null) {
// 即便驗證失敗,也繼續調用過濾鏈,匿名過濾器生成匿名令牌
chain.doFilter(request, response);
return;
} else {
log.info("token令牌驗證成功");
SecurityContextHolder.getContext().setAuthentication(verifyResult);
chain.doFilter(request, response);
}
}
// 從請求頭獲取token
private String getTokenFromRequestHeader(HttpServletRequest request) {
String header = request.getHeader(JWTUtil.TOKEN_HEADER);
if (header == null || !header.startsWith(JWTUtil.TOKEN_PREFIX)) {
log.info("請求頭不含JWT token, 調用下個過濾器");
return null;
}
String token = header.split(" ")[1].trim();
return token;
}
// 驗證token,並生成認證后的token
private UsernamePasswordAuthenticationToken verefyToken(String token, String secret) {
if (token == null) {
return null;
}
// 認證失敗,返回null
if (!JWTUtil.verifyToken(token, secret)) {
return null;
}
// 提取用戶名
String username = JWTUtil.getUsername(token);
// 定義權限列表
List<GrantedAuthority> authorities = new ArrayList<>();
// 從token提取角色
List<String> roles = JWTUtil.getRole(token);
for (String role : roles) {
log.info("用戶身份是:" + role);
authorities.add(new SimpleGrantedAuthority(role));
}
// 構建認證過的token
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
OncePerRequestFilter`保證當前請求中,此過濾器只被調用一次,執行邏輯在`doFilterInternal
代碼實現 security配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AjaxAuthenticationEntryPoint ajaxAuthenticationEntryPoint;
@Autowired
private JWTAuthenticationSuccessHandler jwtAuthenticationSuccessHandler;
@Autowired
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.successHandler(jwtAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.permitAll()
.and()
.addFilterAfter(new JWTAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(ajaxAuthenticationEntryPoint);
}
}
配置里取消了session功能,把我們定義的過濾器添加到過濾鏈中;同時,定義ajaxAuthenticationEntryPoint
處理未認證用戶訪問未授權資源時拋出的異常
@Component
public class AjaxAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
ResponseData responseData = new ResponseData();
responseData.setCode("401");
responseData.setMessage("匿名用戶,請先登錄再訪問!");
httpServletResponse.setCharacterEncoding("utf-8");
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(httpServletResponse.getWriter(), responseData);
}
}
參考
Spring Security3源碼分析(5)-SecurityContextPersistenceFilter分析
Spring Security addFilter() 順序問題
前后端聯調之Form Data與Request Payload,你真的了解嗎?