文章很長,建議收藏起來,慢慢讀! 瘋狂創客圈爲小夥伴奉上如下珍貴的學習資源:html
史上最全 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 + 篇必刷、必刷 的面試題 | 更多 ....., 請參見【 瘋狂創客圈 高併發 總目錄 】 |
springCloud 高質量 博文 | |
---|---|
nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
springcloud + webflux 高併發實戰 | Webflux(史上最全) |
SpringCloud gateway (史上最全) | spring security (史上最全) |
還有 10 + 篇 必刷、必刷 的高質量 博文 | 更多 ....., 請參見【 瘋狂創客圈 高併發 總目錄 】 |
spring security 的核心功能主要包括:前端
其核心就是一組過濾器鏈,項目啓動後將會自動配置。最核心的就是 Basic Authentication Filter 用來認證用戶的身份,一個在spring security中一種過濾器處理一種認證方式。java
好比,對於username password認證過濾器來講,mysql
會檢查是不是一個登陸請求;git
是否包含username 和 password (也就是該過濾器須要的一些認證信息) ;程序員
若是不知足則放行給下一個。github
下一個按照自身職責斷定是不是自身須要的信息,basic的特徵就是在請求頭中有 Authorization:Basic eHh4Onh4 的信息。中間可能還有更多的認證過濾器。最後一環是 FilterSecurityInterceptor,這裏會斷定該請求是否能進行訪問rest服務,判斷的依據是 BrowserSecurityConfig中的配置,若是被拒絕了就會拋出不一樣的異常(根據具體的緣由)。Exception Translation Filter 會捕獲拋出的錯誤,而後根據不一樣的認證方式進行信息的返回提示。web
注意:綠色的過濾器能夠配置是否生效,其餘的都不能控制。面試
首先建立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); } }
假設你但願防止未經受權的用戶訪問「/ 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>
在 Spring boot 應用中使用 Spring Security,用到了 @EnableWebSecurity註解,官方說明爲,該註解和 @Configuration 註解一塊兒使用, 註解 WebSecurityConfigurer 類型的類,或者利用@EnableWebSecurity 註解繼承 WebSecurityConfigurerAdapter的類,這樣就構成了 Spring Security 的配置。
通常狀況,會選擇繼承 WebSecurityConfigurerAdapter 類,其官方說明爲:WebSecurityConfigurerAdapter 提供了一種便利的方式去建立 WebSecurityConfigurer的實例,只須要重寫 WebSecurityConfigurerAdapter 的方法,便可配置攔截什麼URL、設置什麼權限等安全控制。
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(); }
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類的位置添加過濾器 |
/** * {@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。
在application.yaml中定義用戶名密碼:
spring: security: user: name: root password: root
使用root/root登陸,能夠正常訪問/hello
。
@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
@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); }
@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); } }
@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();
獲取當前的登陸信息。
SecurityContext
是安全的上下文,全部的數據都是保存到SecurityContext中。
能夠經過SecurityContext
獲取的對象有:
SecurityContextHolder
用來獲取SecurityContext中保存的數據的工具。經過使用靜態方法獲取SecurityContext的相對應的數據。
SecurityContext context = SecurityContextHolder.getContext();
Authentication表示當前的認證狀況,能夠獲取的對象有:
UserDetails:獲取用戶信息,是否鎖定等額外信息。
Credentials:獲取密碼。
isAuthenticated:獲取是否已經認證過。
Principal:獲取用戶,若是沒有認證,那麼就是用戶名,若是認證了,返回UserDetails。
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
UserDetailsService能夠經過loadUserByUsername獲取UserDetails對象。該接口供spring security進行用戶驗證。
一般使用自定義一個CustomUserDetailsService來實現UserDetailsService接口,經過自定義查詢UserDetails。
AuthenticationManager用來進行驗證,若是驗證失敗會拋出相對應的異常。
密碼加密器。一般是自定義指定。
BCryptPasswordEncoder:哈希算法加密
NoOpPasswordEncoder:不使用加密
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。
因爲先後端不經過保存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須要至少提供兩個方法,一個用來建立咱們的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(); } }
登出沒法對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... }
添加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... }
@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對象。
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) 方法,有三種結果:
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。
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) 來獲取的。
下面是 form 登陸的基本流程:
只要是 form 登陸基本都能轉化爲上面的流程。接下來咱們看看 Spring Security 是如何處理的。
默認它提供了三種登陸方式:
formLogin()
普通表單登陸oauth2Login()
基於 OAuth2.0
認證/受權協議openidLogin()
基於 OpenID
身份認證規範以上三種方式通通是 AbstractAuthenticationFilterConfigurer
實現的,
啓用表單登陸經過兩種方式一種是經過 HttpSecurity
的 apply(C configurer)
方法本身構造一個 AbstractAuthenticationFilterConfigurer
的實現,這種是比較高級的玩法。 另外一種是咱們常見的使用 HttpSecurity
的 formLogin()
方法來自定義 FormLoginConfigurer
。咱們先搞一下比較常規的第二種。
該類是 form 表單登陸的配置類。它提供了一些咱們經常使用的配置方法:
/login
。Action
,再由過濾器UsernamePasswordAuthenticationFilter
攔截處理,該 Action
其實不會處理任何邏輯。username
。password
Controller
(控制器)來處理返回值,可是要注意 RequestMethod
。alwaysUse
爲 true
只要進行認證流程並且成功,會一直跳轉到此。通常推薦默認值 false
defaultSuccessUrl
的 alwaysUse
爲 true
可是要注意 RequestMethod
。success
方式failure
方式知道了這些咱們就能來搞個定製化的登陸了。
接下來是咱們最激動人心的實戰登陸操做。 有疑問的可認真閱讀 Spring 實戰 的一系列預熱文章。
咱們的接口訪問都要經過認證,登錄錯誤後返回錯誤信息(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" }
就這麼完了了麼?如今登陸的花樣繁多。常規的就有短信、郵箱、掃碼 ,第三方是之後我要講的不在今天範圍以內。 如何應對想法多的產品經理? 咱們來搞一個可擴展各類姿式的登陸方式。咱們在上面 2. form 登陸的流程 中的 用戶 和 斷定 之間增長一個適配器來適配便可。 咱們知道這個所謂的 斷定就是 UsernamePasswordAuthenticationFilter
。
咱們只須要保證 uri 爲上面配置的/process 而且可以經過 getParameter(String name) 獲取用戶名和密碼便可 。
我忽然以爲能夠模仿 DelegatingPasswordEncoder
的搞法, 維護一個註冊表執行不一樣的處理策略。固然咱們要實現一個 GenericFilterBean
在 UsernamePasswordAuthenticationFilter
以前執行。同時制定登陸的策略。
定義登陸方式枚舉 ``。
public enum LoginTypeEnum { /** * 原始登陸方式. */ FORM, /** * Json 提交. */ JSON, /** * 驗證碼. */ CAPTCHA }
定義前置處理器接口用來處理接收的各類特點的登陸參數 並處理具體的邏輯。這個藉口其實有點隨意 ,重要的是你要學會思路。我實現了一個 默認的 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); }
該過濾器維護了 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); } }
經過 POST
表單提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0
能夠請求成功。或者如下列方式也能夠提交成功:
更多的方式 只須要實現接口 LoginPostProcessor
注入 PreLoginFilter
JWT是JSON Web Token的縮寫,是目前最流行的跨域認證解決方法。
互聯網服務認證的通常流程是:
上面的認證模式,存在如下缺點:
JWT認證原理是:
Authorization
裏JWT token令牌能夠包含用戶身份、登陸時間等信息,這樣登陸狀態保持者由服務器端變爲客戶端,服務器變成無狀態了;token放到請求頭,實現了跨域
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT由三部分組成:
表現形式爲: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個官方字段:
固然,用戶也能夠定義私有字段。
這個 JSON 對象也要使用 Base64URL 算法轉成字符串
Signature
Signature 部分是對前兩部分的簽名,防止數據篡改
簽名算法以下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret )
算出簽名之後,把 Header、Payload、Signature 三個部分拼成一個字符串,每一個部分之間用"."分隔
Security是基於AOP和Servlet過濾器的安全框架,爲了實現JWT要重寫那些方法、自定義那些過濾器須要首先了解security自帶的過濾器。security默認過濾器鏈以下:
這個過濾器有兩個做用:
因爲禁用session功能,因此該過濾器只剩一個做用即把SecurityContextHolder的securitycontext清空。舉例來講明爲什麼要清空securitycontext:用戶1發送一個請求,由線程M處理,當響應完成線程M放回線程池;用戶2發送一個請求,本次請求一樣由線程M處理,因爲securitycontext沒有清空,理應儲存用戶2的信息但此時儲存的是用戶1的信息,形成用戶信息不符
UsernamePasswordAuthenticationFilter
繼承自AbstractAuthenticationProcessingFilter
,處理邏輯在doFilter
方法中:
UsernamePasswordAuthenticationFilter
攔截時,判斷請求路徑是否匹配登陸URL,若不匹配繼續執行下個過濾器;不然,執行步驟2attemptAuthentication
方法進行認證。UsernamePasswordAuthenticationFilter
重寫了attemptAuthentication
方法,負責讀取表單登陸參數,委託AuthenticationManager
進行認證,返回一個認證過的token(null表示認證失敗)successfulAuthentication
。該方法把認證過的token放入securitycontext供後續請求受權,同時該方法預留一個擴展點(AuthenticationSuccessHandler.onAuthenticationSuccess方法
),進行認證成功後的處理uthenticationFailureHandler.onAuthenticationFailure
進行認證失敗後的處理UsernamePasswordAuthenticationFilter
的attemptAuthentication
方法,執行邏輯以下:
HttpServletRequest.getParameter
方法獲取參數,它只能處理Content-Type爲application/x-www-form-urlencoded或multipart/form-data的請求,如果application/json則沒法獲取值UsernamePasswordAuthenticationToken
對象,建立未認證的token。UsernamePasswordAuthenticationToken
有兩個重載的構造方法,其中public UsernamePasswordAuthenticationToken(Object principal, Object credentials)
建立未經認證的token,public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
建立已認證的tokenAuthenticationManager
,其缺省實現爲ProviderManager
,調用其authenticate
進行認證ProviderManager
的authenticate
是個模板方法,它遍歷全部AuthenticationProvider
,直至找到支持認證某類型token的AuthenticationProvider
,調用AuthenticationProvider.authenticate
方法認證,AuthenticationProvider.authenticate
加載正確的帳號、密碼進行比較驗證AuthenticationManager.authenticate
方法返回一個已認證的tokenAnonymousAuthenticationFilter
負責建立匿名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認證思路:
AuthenticationSuccessHandler
認證成功處理器,由該處理器生成token令牌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
@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,你真的瞭解嗎?