spring security (史上最全)

文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈爲小夥伴奉上如下珍貴的學習資源:html


推薦2:史上最全 Java 面試題 21 個專題

史上最全 Java 面試題 21 個專題 阿里、京東、美團、頭條.... 隨意挑、橫着走!!!
Java基礎
1: JVM面試題(史上最強、持續更新、吐血推薦) http://www.javashuo.com/article/p-vhnpdnhb-vd.html
2:Java基礎面試題(史上最全、持續更新、吐血推薦) http://www.javashuo.com/article/p-otujhkjp-vd.html
3:死鎖面試題(史上最強、持續更新) [http://www.javashuo.com/article/p-uyudvdol-vd.html]
4:設計模式面試題 (史上最全、持續更新、吐血推薦) http://www.javashuo.com/article/p-qnkzhtsu-vd.html
5:架構設計面試題 (史上最全、持續更新、吐血推薦) http://www.javashuo.com/article/p-dlpjqbmg-vd.html
還有 10 +必刷、必刷 的面試題 更多 ....., 請參見【 瘋狂創客圈 高併發 總目錄

推薦3: 瘋狂創客圈 高併發 高質量博文

springCloud 高質量 博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
springcloud + webflux 高併發實戰 Webflux(史上最全)
SpringCloud gateway (史上最全) spring security (史上最全)
還有 10 +必刷、必刷 的高質量 博文 更多 ....., 請參見【 瘋狂創客圈 高併發 總目錄

1、spring security 簡介

​ spring security 的核心功能主要包括:前端

  • 認證 (你是誰)
  • 受權 (你能幹什麼)
  • 攻擊防禦 (防止僞造身份)

​ 其核心就是一組過濾器鏈,項目啓動後將會自動配置。最核心的就是 Basic Authentication Filter 用來認證用戶的身份,一個在spring security中一種過濾器處理一種認證方式。java

img

好比,對於username password認證過濾器來講,mysql

會檢查是不是一個登陸請求;git

是否包含username 和 password (也就是該過濾器須要的一些認證信息) ;程序員

若是不知足則放行給下一個。github

​ 下一個按照自身職責斷定是不是自身須要的信息,basic的特徵就是在請求頭中有 Authorization:Basic eHh4Onh4 的信息。中間可能還有更多的認證過濾器。最後一環是 FilterSecurityInterceptor,這裏會斷定該請求是否能進行訪問rest服務,判斷的依據是 BrowserSecurityConfig中的配置,若是被拒絕了就會拋出不一樣的異常(根據具體的緣由)。Exception Translation Filter 會捕獲拋出的錯誤,而後根據不一樣的認證方式進行信息的返回提示。web

注意:綠色的過濾器能夠配置是否生效,其餘的都不能控制。面試

2、入門項目

​ 首先建立spring boot項目HelloSecurity,其pom主要依賴以下:ajax

<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);
    }
}

二、加入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>

3、參數詳解

一、註解 @EnableWebSecurity

​ 在 Spring boot 應用中使用 Spring Security,用到了 @EnableWebSecurity註解,官方說明爲,該註解和 @Configuration 註解一塊兒使用, 註解 WebSecurityConfigurer 類型的類,或者利用@EnableWebSecurity 註解繼承 WebSecurityConfigurerAdapter的類,這樣就構成了 Spring Security 的配置。

二、抽象類 WebSecurityConfigurerAdapter

​ 通常狀況,會選擇繼承 WebSecurityConfigurerAdapter 類,其官方說明爲:WebSecurityConfigurerAdapter 提供了一種便利的方式去建立 WebSecurityConfigurer的實例,只須要重寫 WebSecurityConfigurerAdapter 的方法,便可配置攔截什麼URL、設置什麼權限等安全控制。

三、方法 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();
    }

四、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類的位置添加過濾器

五、類 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);
}

添加數據庫數據

image-20201130200749622

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。

    image-20201202115120854

  • @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對象。

7、原理講解

一、校驗流程圖

img

二、源碼分析

  • 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) 方法,有三種結果:

  1. 返回一個 Authentication 對象。配置的 SessionAuthenticationStrategy` 將被調用,而後 而後調用 successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
  2. 驗證時發生 AuthenticationException。unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法將被調用。
  3. 返回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) 來獲取的。

8、玩轉自定義登陸

1. form 登陸的流程

下面是 form 登陸的基本流程:

img

只要是 form 登陸基本都能轉化爲上面的流程。接下來咱們看看 Spring Security 是如何處理的。

3. Spring Security 中的登陸

默認它提供了三種登陸方式:

  • formLogin() 普通表單登陸
  • oauth2Login() 基於 OAuth2.0 認證/受權協議
  • openidLogin() 基於 OpenID 身份認證規範

以上三種方式通通是 AbstractAuthenticationFilterConfigurer 實現的,

4. HttpSecurity 中的 form 表單登陸

啓用表單登陸經過兩種方式一種是經過 HttpSecurityapply(C configurer) 方法本身構造一個 AbstractAuthenticationFilterConfigurer 的實現,這種是比較高級的玩法。 另外一種是咱們常見的使用 HttpSecurityformLogin() 方法來自定義 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) 默認登錄成功後跳轉到此 ,若是 alwaysUsetrue 只要進行認證流程並且成功,會一直跳轉到此。通常推薦默認值 false
  • successForwardUrl(String forwardUrl) 效果等同於上面 defaultSuccessUrlalwaysUsetrue 可是要注意 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 的搞法, 維護一個註冊表執行不一樣的處理策略。固然咱們要實現一個 GenericFilterBeanUsernamePasswordAuthenticationFilter 以前執行。同時制定登陸的策略。

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 。經過 HttpSecurityaddFilterBefore(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 能夠請求成功。或者如下列方式也能夠提交成功:

img

更多的方式 只須要實現接口 LoginPostProcessor 注入 PreLoginFilter

九 整合JWT作登陸認證

JWT是JSON Web Token的縮寫,是目前最流行的跨域認證解決方法。

互聯網服務認證的通常流程是:

  1. 用戶向服務器發送帳號、密碼
  2. 服務器驗證經過後,將用戶的角色、登陸時間等信息保存到當前會話中
  3. 同時,服務器向用戶返回一個session_id(通常保存在cookie裏)
  4. 用戶再次發送請求時,把含有session_id的cookie發送給服務器
  5. 服務器收到session_id,查找session,提取用戶信息

上面的認證模式,存在如下缺點:

  • cookie不容許跨域
  • 由於每臺服務器都必須保存session對象,因此擴展性很差

JWT認證原理是:

  1. 用戶向服務器發送帳號、密碼
  2. 服務器驗證經過後,生成token令牌返回給客戶端(token能夠包含用戶信息)
  3. 用戶再次請求時,把token放到請求頭Authorization
  4. 服務器收到請求,驗證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默認過濾器鏈以下:

  1. org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
  2. org.springframework.security.web.context.SecurityContextPersistenceFilter
  3. org.springframework.security.web.header.HeaderWriterFilter
  4. org.springframework.security.web.authentication.logout.LogoutFilter
  5. org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
  6. org.springframework.security.web.savedrequest.RequestCacheAwareFilter
  7. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
  8. org.springframework.security.web.authentication.AnonymousAuthenticationFilter
  9. org.springframework.security.web.session.SessionManagementFilter
  10. org.springframework.security.web.access.ExceptionTranslationFilter
  11. 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方法中:

  1. 當請求被UsernamePasswordAuthenticationFilter攔截時,判斷請求路徑是否匹配登陸URL,若不匹配繼續執行下個過濾器;不然,執行步驟2
  2. 調用attemptAuthentication方法進行認證。UsernamePasswordAuthenticationFilter重寫了attemptAuthentication方法,負責讀取表單登陸參數,委託AuthenticationManager進行認證,返回一個認證過的token(null表示認證失敗)
  3. 判斷token是否爲null,非null表示認證成功,null表示認證失敗
  4. 若認證成功,調用successfulAuthentication。該方法把認證過的token放入securitycontext供後續請求受權,同時該方法預留一個擴展點(AuthenticationSuccessHandler.onAuthenticationSuccess方法),進行認證成功後的處理
  5. 若認證失敗,一樣能夠擴展uthenticationFailureHandler.onAuthenticationFailure進行認證失敗後的處理
  6. 只要當前請求路徑匹配登陸URL,那麼不管認證成功仍是失敗,當前請求都會響應完成,再也不執行過濾器鏈

UsernamePasswordAuthenticationFilterattemptAuthentication方法,執行邏輯以下:

  1. 從請求中獲取表單參數。由於使用HttpServletRequest.getParameter方法獲取參數,它只能處理Content-Type爲application/x-www-form-urlencoded或multipart/form-data的請求,如果application/json則沒法獲取值
  2. 把步驟1獲取的帳號、密碼封裝成UsernamePasswordAuthenticationToken對象,建立未認證的token。UsernamePasswordAuthenticationToken有兩個重載的構造方法,其中public UsernamePasswordAuthenticationToken(Object principal, Object credentials)建立未經認證的token,public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)建立已認證的token
  3. 獲取認證管理器AuthenticationManager,其缺省實現爲ProviderManager,調用其authenticate進行認證
  4. ProviderManagerauthenticate是個模板方法,它遍歷全部AuthenticationProvider,直至找到支持認證某類型token的AuthenticationProvider,調用AuthenticationProvider.authenticate方法認證,AuthenticationProvider.authenticate加載正確的帳號、密碼進行比較驗證
  5. 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認證思路:

  1. 利用Security原生的表單認證過濾器驗證用戶名、密碼
  2. 驗證經過後自定義AuthenticationSuccessHandler認證成功處理器,由該處理器生成token令牌

JWT受權思路:

  1. 使用JWT目的是讓服務器變成無狀態,不用session共享數據,因此要禁用security的session功能(http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS))
  2. token令牌數據結構設計時,payload部分要儲存用戶名、角色信息
  3. token令牌有兩個做用:
    1. 認證, 用戶發送的token合法即表明認證成功
    2. 受權,令牌驗證成功後提取角色信息,構造認證過的token,將其放到securitycontext,具體權限判斷交給security框架處理
  4. 本身實現一個過濾器,攔截用戶請求,實現(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);
    }
}

參考

JSON Web Token 入門教程

Spring Security-5-認證流程梳理

Spring Security3源碼分析(5)-SecurityContextPersistenceFilter分析

Spring Security addFilter() 順序問題

先後端聯調之Form Data與Request Payload,你真的瞭解嗎?

Spring Boot 2 + Spring Security 5 + JWT 的單頁應用 Restful 解決方案

SpringBoot實戰派-第十章源碼

相關文章
相關標籤/搜索