1 環境搭建
1.1 創建一個SpringBoot項目
1.2 創建一個Restful接口
新建一個Controller類即可

package com.example.wiremock.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author 王楊帥 * @create 2018-05-12 21:18 * @desc **/ @RestController @RequestMapping(value = "/security") @Slf4j public class SecurityController { @GetMapping(value = "/connect") public String connect() { String result = "前后台連接成功"; log.info("===" + result); return result; } }
1.3 引入SpringSecurity相關jar包
<!--security相關--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
1.4 啟動項目
技巧01:由於我們引入了SpringSecurity相關的jar包,所以系統會默認開啟SpringSecurity的相關配置
技巧02:可以在配置文件中關掉這個配置(即:使SpringSecurity失效)
技巧03:啟動項目后會在控制台上打印出一個密碼,因為默認的SpringSecurity配置會對所有的請求都進行權限驗證,如果不通過就會跳轉到 /login 請求,則是一個登陸頁面或者一個登陸彈出窗口,默認登陸名為 user,默認登陸密碼就是啟動項目是控制台打印出來的字符串
1.5 訪問接口
IP + 端口 + 上下文p徑 + 請求路徑
http://127.0.0.1:9999/dev/security/connect
技巧01:SpringSecurity默認的配置會默認對所有的請求都進行權限驗證,所以會跳轉到 /login 請求路徑,畫面如下;輸入正確的用戶名和密碼后跳轉到之的請求所得到的響應
1.6 SpringSecurity的授權流程
所有請求url -> BasicAuthenticationFilter / UsernamePasswordAuthenticationFilter -> FilterSecurityInterceptor -> BasicAuthenticationFilter / UsernamePasswordAuthenticationFilter -> FilterSecurityInterceptor -> controller層
所有請求都默認進入 BasicAuthenticationFilter 過濾器進行過濾,然后進入 FilterSecurityInterceptor 過濾器進行權限驗證,如果在 FilterSecurityInterceptor 中權限驗證就會跳轉到 /login 請求進行處理,然后在進入 BasicAuthenticationFilter 或者 UsernamePasswordAuthenticationFilter 過濾器,再進入 FilterSecurityInterceptor,只有當 FilterSecurityInterceptor 過濾通過了才會跳轉到之前的請求路徑
技巧01:如果在 FilterSecurityInterceptor 中拋出了異常就會跳轉到 ExceptionTranslationFilter 進行相應的處理
2 SpringSecurity驗證
直接使用SpringSecurity默認的配置進行權限驗證時只有一個用戶,無法滿足實際開發需求;在實際的開發中需要根據不同的用戶判斷其權限
技巧01:直接繼承一個UserDetailsService接口即可;該接口中有一個 loadUserByUsername 方法,該方法是通過用戶名查找用戶信息,然后在根據查到的用戶信息來判斷該用戶的權限,該方法返回一個實現了UserDetailsService接口的User對象
2.1 實現 UserDetailsService
技巧01:實現了 UserDetailsService接口的實現類必須在類級別上添加@Component注解,目的上讓Spring容器去管理這個Bean
技巧02:可以在實現了 UserDetailsService接口的實現類中依賴注入其他Bean(例如:依賴注入持久層Bean來實現數據庫操作)
技巧03:如果實現了 UserDetailsService 接口就必須進行 SpringSecurity 配置,因為SpringSecurity會使用一個實現了PasswordEncoder接口的實現類去比較用戶錄入的密碼和從數據庫中獲取到的密碼是否相等
技巧04:實現了 UserDetailsService接口的實現類就是自定義的認證方式,需要將自定義的認證方式添加到認證管理構建中心,否則自定義認證方式不會生效

package com.example.wiremock.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; //import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; /** * @author 王楊帥 * @create 2018-05-12 22:09 * @desc **/ @Component @Slf4j public class FuryUserDetailService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; // 01 依賴注入持久層(用於查找用戶信息) @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { log.info("用戶名:" + username); // 技巧01: /login 請求傳過來的用戶名會傳到這里 // 02 根據用戶名到數據庫中查找數據 String pwd = passwordEncoder.encode("123321"); // 技巧02:此處是模擬的從數據庫中查詢到的密碼 // 03 返回一個 User 對象(技巧01:這個User對象時實現了UserDetail接口的,這里利用的是Spring框架提供的User對象,也可以使用自定義但是實現了UserDetai接口的User對象) return new User(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
2.2 SpringSecurity配置
技巧01:其實就是配置一個Bean而已,只不過這個Bean的返回類型是 PasswordEncoder 類型
技巧02:可以使用實現了 PasswordEncoder接口的實現類 BCryptPasswordEncoder 作為返回類型,也可以使用自定義並且實現了 PasswordEncoder接口的類作為返回類型

package com.xunyji.springsecurity01.config; //import com.xunyji.springsecurity01.service.FuryUserDetailService; import com.xunyji.springsecurity01.service.FuryUserDetailService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 王楊帥 * @create 2018-09-07 22:18 * @desc **/ @Configuration //@EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private FuryUserDetailService furyUserDetailService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 內存認證 start // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("admin") // .password("123321") // .roles("ADMIN"); // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("wys") // .password("123321") // .roles("USER"); // 內存認證 end auth.authenticationProvider(authenticationProvider()); // 添加自定義的認證邏輯 } /** * 創建認證提供者Bean * 技巧01:DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider實現類 * @return */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); // 創建DaoAuthenticationProvider實例 authProvider.setUserDetailsService(furyUserDetailService); // 將自定義的認證邏輯添加到DaoAuthenticationProvider authProvider.setPasswordEncoder(passwordEncoder); // 設置自定義的密碼加密 return authProvider; } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/test/home").permitAll() .anyRequest().authenticated() .and() .logout().permitAll() .and() .formLogin(); http.csrf().disable(); // 關閉csrf驗證 } /** * 創建PasswordEncoder對應的Bean * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // 使用springSecurity提供的密碼加密匹配類 // return new MyPasswordEncoder(); // 使用自定義的密碼加密匹配類 } }
2.3 自定義加密類
就是一個實現了 PasswordEncoder接口的類而已,我們可以通過該類來實現MD5加密或者一些其他的加密方式
2.3.2 加密類
用於實現自己的加密算法

package com.example.wiremock.config; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 王楊帥 * @create 2018-05-12 22:41 * @desc **/ public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { if (charSequence.toString().equals(s)) { return true; } return false; } }
2.3.3 重新進行SrpingSecurity配置

package com.xunyji.springsecurity01.config; //import com.xunyji.springsecurity01.service.FuryUserDetailService; import com.xunyji.springsecurity01.service.FuryUserDetailService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * @author 王楊帥 * @create 2018-09-07 22:18 * @desc **/ @Configuration //@EnableWebSecurity public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private FuryUserDetailService furyUserDetailService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 內存認證 start // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("admin") // .password("123321") // .roles("ADMIN"); // auth.inMemoryAuthentication() // .passwordEncoder(new MyPasswordEncoder()) // .withUser("wys") // .password("123321") // .roles("USER"); // 內存認證 end auth.authenticationProvider(authenticationProvider()); // 添加自定義的認證邏輯 } /** * 創建認證提供者Bean * 技巧01:DaoAuthenticationProvider是SpringSecurity提供的AuthenticationProvider實現類 * @return */ @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); // 創建DaoAuthenticationProvider實例 authProvider.setUserDetailsService(furyUserDetailService); // 將自定義的認證邏輯添加到DaoAuthenticationProvider authProvider.setPasswordEncoder(passwordEncoder); // 設置自定義的密碼加密 return authProvider; } @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/test/home").permitAll() .anyRequest().authenticated() .and() .logout().permitAll() .and() .formLogin(); http.csrf().disable(); // 關閉csrf驗證 } /** * 創建PasswordEncoder對應的Bean * @return */ @Bean public PasswordEncoder passwordEncoder() { // return new BCryptPasswordEncoder(); // 使用springSecurity提供的密碼加密匹配類 return new MyPasswordEncoder(); // 使用自定義的密碼加密匹配類 } }
2.4 測試
啟動項目后進入到登錄頁面
技巧01:隨便輸入一個用戶名(PS:由於后台沒有實現根據用戶名查詢用戶信息的邏輯,若依隨便輸入一個即可),輸入一個固定的密碼(PS:這個密碼要和loadUserByUsername方法中返回的User對象中的password參數加密前的內容一致)
2.5 進階
loadUserByUsername 方法的返回類型是一個User對象,這個User對象有兩個默認的構造器,一個僅僅包含用戶名、用戶秘密和權限,另一個除了包含這些信息還包含一些用戶的有效性信息
技巧01:直接看 UserDetails 就知道了

// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.Assert; public class User implements UserDetails, CredentialsContainer { private static final long serialVersionUID = 500L; private static final Log logger = LogFactory.getLog(User.class); private String password; private final String username; private final Set<GrantedAuthority> authorities; private final boolean accountNonExpired; private final boolean accountNonLocked; private final boolean credentialsNonExpired; private final boolean enabled; public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities); } public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (username != null && !"".equals(username) && password != null) { this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); } else { throw new IllegalArgumentException("Cannot pass null or empty values to constructor"); } } public Collection<GrantedAuthority> getAuthorities() { return this.authorities; } public String getPassword() { return this.password; } public String getUsername() { return this.username; } public boolean isEnabled() { return this.enabled; } public boolean isAccountNonExpired() { return this.accountNonExpired; } public boolean isAccountNonLocked() { return this.accountNonLocked; } public boolean isCredentialsNonExpired() { return this.credentialsNonExpired; } public void eraseCredentials() { this.password = null; } private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) { Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection"); SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new User.AuthorityComparator()); Iterator var2 = authorities.iterator(); while(var2.hasNext()) { GrantedAuthority grantedAuthority = (GrantedAuthority)var2.next(); Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements"); sortedAuthorities.add(grantedAuthority); } return sortedAuthorities; } public boolean equals(Object rhs) { return rhs instanceof User ? this.username.equals(((User)rhs).username) : false; } public int hashCode() { return this.username.hashCode(); } public String toString() { StringBuilder sb = new StringBuilder(); sb.append(super.toString()).append(": "); sb.append("Username: ").append(this.username).append("; "); sb.append("Password: [PROTECTED]; "); sb.append("Enabled: ").append(this.enabled).append("; "); sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; "); sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired).append("; "); sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; "); if (!this.authorities.isEmpty()) { sb.append("Granted Authorities: "); boolean first = true; Iterator var3 = this.authorities.iterator(); while(var3.hasNext()) { GrantedAuthority auth = (GrantedAuthority)var3.next(); if (!first) { sb.append(","); } first = false; sb.append(auth); } } else { sb.append("Not granted any authorities"); } return sb.toString(); } public static User.UserBuilder withUsername(String username) { return builder().username(username); } public static User.UserBuilder builder() { return new User.UserBuilder(); } /** @deprecated */ @Deprecated public static User.UserBuilder withDefaultPasswordEncoder() { logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications."); PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); User.UserBuilder var10000 = builder(); encoder.getClass(); return var10000.passwordEncoder(encoder::encode); } public static User.UserBuilder withUserDetails(UserDetails userDetails) { return withUsername(userDetails.getUsername()).password(userDetails.getPassword()).accountExpired(!userDetails.isAccountNonExpired()).accountLocked(!userDetails.isAccountNonLocked()).authorities(userDetails.getAuthorities()).credentialsExpired(!userDetails.isCredentialsNonExpired()).disabled(!userDetails.isEnabled()); } public static class UserBuilder { private String username; private String password; private List<GrantedAuthority> authorities; private boolean accountExpired; private boolean accountLocked; private boolean credentialsExpired; private boolean disabled; private Function<String, String> passwordEncoder; private UserBuilder() { this.passwordEncoder = (password) -> { return password; }; } public User.UserBuilder username(String username) { Assert.notNull(username, "username cannot be null"); this.username = username; return this; } public User.UserBuilder password(String password) { Assert.notNull(password, "password cannot be null"); this.password = password; return this; } public User.UserBuilder passwordEncoder(Function<String, String> encoder) { Assert.notNull(encoder, "encoder cannot be null"); this.passwordEncoder = encoder; return this; } public User.UserBuilder roles(String... roles) { List<GrantedAuthority> authorities = new ArrayList(roles.length); String[] var3 = roles; int var4 = roles.length; for(int var5 = 0; var5 < var4; ++var5) { String role = var3[var5]; Assert.isTrue(!role.startsWith("ROLE_"), role + " cannot start with ROLE_ (it is automatically added)"); authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); } return this.authorities((Collection)authorities); } public User.UserBuilder authorities(GrantedAuthority... authorities) { return this.authorities((Collection)Arrays.asList(authorities)); } public User.UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) { this.authorities = new ArrayList(authorities); return this; } public User.UserBuilder authorities(String... authorities) { return this.authorities((Collection)AuthorityUtils.createAuthorityList(authorities)); } public User.UserBuilder accountExpired(boolean accountExpired) { this.accountExpired = accountExpired; return this; } public User.UserBuilder accountLocked(boolean accountLocked) { this.accountLocked = accountLocked; return this; } public User.UserBuilder credentialsExpired(boolean credentialsExpired) { this.credentialsExpired = credentialsExpired; return this; } public User.UserBuilder disabled(boolean disabled) { this.disabled = disabled; return this; } public UserDetails build() { String encodedPassword = (String)this.passwordEncoder.apply(this.password); return new User(this.username, encodedPassword, !this.disabled, !this.accountExpired, !this.credentialsExpired, !this.accountLocked, this.authorities); } } private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable { private static final long serialVersionUID = 500L; private AuthorityComparator() { } public int compare(GrantedAuthority g1, GrantedAuthority g2) { if (g2.getAuthority() == null) { return -1; } else { return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority()); } } } }

// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.core.userdetails; import java.io.Serializable; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
3 SpringSecurity配置詳解
3.1 默認配置簡述
默認的SpringSecurity會對所有都請求都進行權限驗證,而且只包含user用戶的用戶信息,而在實際的開發中需要過濾掉某些請求(例如:登錄請求);如果我們實現了UserDetailsService接口,那就必須進行SpringSecurity配置,因為實現了UserDetailsService接口后需要用到一個返回類型是PasswordEncoder的Bean,而這個Bean必須在SpringSecurity的配置文件中進行配置
3.2 配置詳情
技巧01:在繼承了WebSecurityConfigurerAdapter的子類中重寫 configure 方法即可
坑01:configure() 方法有多個重載方法,我們需要重寫參數類型是 HttpSecurity 那個重載方法
3.2.1 配置登錄方式
技巧01:如果重寫了 configure() 方法后之前默認的SpringSecurity配置就會失效(例如:對所有請求都進行權限驗證),我們需要自定義對那些請求進行權限驗證
登錄方式有兩種:
》表單登錄方式
提交后會被 UsernamePasswordAuthenticationFilter 過濾
》彈出彈出窗口登錄方式
登錄后會被 BasicAuthenticationFilter 過濾
@Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() // 配置彈出框登錄 .and().authorizeRequests() // 請求權限設置 .anyRequest() // 所有請求 .authenticated(); // 所有請求都進行權限驗證 }

package com.example.wiremock.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic() // 配置彈出框登錄 .and().authorizeRequests() // 請求權限設置 .anyRequest() // 所有請求 .authenticated(); // 所有請求都進行權限驗證 } }
3.2.2 跨站防護
主要用於前后端分離時利用前端的登錄頁面進行模擬登錄

package com.example.wiremock.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置彈出框登錄 http.formLogin() .and().authorizeRequests() // 請求權限設置 .anyRequest() // 所有請求 .authenticated() // 所有請求都進行權限驗證 .and().csrf().disable(); // 取消“跨站防護” } }
3.2.3 自定義登錄頁面和登錄提交路徑
3.2.3.1 自定義登錄頁面
創建一個名為 xiangxu-login.html 的HTML文件作為自定義的登錄頁面

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h2>自定義表單登陸</h2> <form action="/login" method="post"> <table> <tr> <td>用戶名:</td> <td> <input type="text" name="username" /> </td> </tr> <tr> <td>密碼:</td> <td> <input type="password" name="password" /> </td> </tr> <tr> <td colspan="2"> <button type="submit">登陸</button> </td> </tr> </table> </form> </body> </html>
技巧01:表單請求字段必須包含username和password,表單提交方式為POST,默認的表單提交路徑為 /login
技巧02:如果想要自定義的表單登錄頁面就需要進行SpringSecurity配置
技巧03:該HTML文件必須放在resources目錄下的resources文件夾下,形如
3.2.3.2 自定義登錄路徑
修改自定義頁面中表單的請求路徑,默認是 /login,本案例修改成:/dev/xiangxu/login
3.2.3.3 配置SpringSecurity
如果想要讓自定義的登錄頁面和自定義的登錄請求路徑生效就必須進行SpringSecurity配置
技巧01:配置 loginPage 時,如果項目配置了上下文路徑就必須加上上下文及路徑,否則就找不到放在resources目錄下resources文件夾中的請求頁面
技巧02:配置 loginProcessingUrl 時就是配置請求頁面表單的登錄路徑,表單怎么寫的,這里的配置就怎么寫
坑01:雖然都對自定義的登錄頁面以及登錄請求進行了配置,但是會出現反復重定向的問題;因為我們的SrpingSecurity配置是會對所有的請求路徑都進行權限校驗的,所以我們必須排除掉對 登錄請求頁面 和 登錄請求路徑 的權限驗證
3.2.4 登錄成功后響應信息
默認的SpringSecurity配置在登錄成功后就會跳轉到之前的請求,登錄失敗就會繼續跳轉到登錄頁面
需求:基於RestfulAPI的前后端分離項目需要登錄成功后直接返回JSON格式的響應即可,具體怎么跳轉由前端去控制
技巧01:響應自定義登錄成功和登錄失敗的響應格式只需要實現兩個接口並進行SpringSecurity配置即可
》AuthenticationSuccessHandler
AuthenticationSuccessHandler是登錄成功的接口

package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class XiangXuAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // TODO Auto-generated method stub log.info("登陸成功"); response.setContentType("application/json;charset=UTF-8"); // 響應類型 response.getWriter().write(objectMapper.writeValueAsString(authentication)); // 數據轉化成json類型后再進行響應操作 } }
》AuthenticationFailureHandler
AuthenticationFailureHandler是登錄失敗的接口

package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component public class XiangXuAuthenticationFailureHandler implements AuthenticationFailureHandler { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper ObjectMapper; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("登陸失敗"); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(ObjectMapper.writeValueAsString(exception));; } }
》SpringSecurity配置

package com.example.wiremock.config; import com.example.wiremock.authentication.XiangXuAuthenticationFailureHandler; import com.example.wiremock.authentication.XiangXuAuthenticationSuccessHandler; import com.example.wiremock.entity.properties.SecurityProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.net.SecureCacheResponse; @Configuration @Slf4j public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityProperty securityProperty; @Autowired private XiangXuAuthenticationSuccessHandler xiangXuAuthenticationSuccessHandler; @Autowired private XiangXuAuthenticationFailureHandler xiangXuAuthenticationFailureHandler; private String LOGIN_PAGE; @PostConstruct public void init() { LOGIN_PAGE = securityProperty.getBrowser().getLoginPage(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置彈出框登錄 http.formLogin() // .loginPage("/dev/xiangxu-login.html") .loginPage("/dev/authentication/require") // 自定義登陸頁面 // .loginPage(LOGIN_PAGE) .loginProcessingUrl("/dev/xiangxu/login") // 自定義登陸請求路徑(默認是:/login) .successHandler(xiangXuAuthenticationSuccessHandler) // 自定義成功處理器 .failureHandler(xiangXuAuthenticationFailureHandler) // 自定義失敗處理器 .and().authorizeRequests() // 請求權限設置 // .antMatchers("/dev/xiangxu-login.html").permitAll() .antMatchers("/dev/authentication/require").permitAll() .antMatchers(LOGIN_PAGE).permitAll() .antMatchers("/dev/xiangxu/login").permitAll() .anyRequest() // 所有請求 .authenticated() // 所有請求都進行權限驗證 .and().csrf().disable(); // 取消“跨站防護” } }
4 SpringSecurity配置進階
4.1 登錄頁面個性化配置
需求:開發者僅僅需要在properties文件中按照規定的格式配置登錄頁面即可,例如
xiangxu.security.browser.loginPage = /dev/xiangxu-login.html
4.1.1 properties文件配置
在properties文件中配置登錄頁面

#security.basic.enabled = false
xiangxu.security.browser.loginPage = /dev/xiangxu-login.html
4.1.2 創建實體類
根據properties文件的配置格式創建實體類,參考博文 -> 點擊前往
》SecurityProperty 實體類
技巧01:為 browser 屬性設置初始值,目的是為了依賴注入后在使用依賴注入對象時出現空指針異常

package com.example.wiremock.entity.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @author 王楊帥 * @create 2018-05-13 13:45 * @desc **/ @ConfigurationProperties(prefix = "xiangxu.security") public class SecurityProperty { private BrowserProperty browser = new BrowserProperty(); public BrowserProperty getBrowser() { return browser; } public void setBrowser(BrowserProperty browser) { this.browser = browser; } }
》BrowserProperty 實體類
技巧01:為 loginPage 屬性設置初始值,目的是為了在 properties 文件中不進行登錄頁面配置時使用默認指定的登錄頁面(PS:不是SpringSecurity指定的表單登錄頁面,而是我們自己開發的登錄頁面)

package com.example.wiremock.entity.properties; /** * @author 王楊帥 * @create 2018-05-13 13:46 * @desc **/ public class BrowserProperty { private String loginPage = "/login.html"; public String getLoginPage() { return loginPage; } public void setLoginPage(String loginPage) { this.loginPage = loginPage; } }
》實體類配置類

package com.example.wiremock.config; import com.example.wiremock.entity.properties.SecurityProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @EnableConfigurationProperties(SecurityProperty.class) public class PropertiesConfig { }
4.1.3 SpringSecurity配置
》在配置類中依賴注入SecurityProperty
》定義一個LOGIN_PAGE屬性來存放登錄頁面路徑
》初始化LOGIN_PAGE
技巧01:不能利用依賴注入的對象去初始化成員變量,解決辦法是創建一個初始化方法,在這個初始化方法中利用依賴注入的去初始化成員變量,但是這個初始化方法必須添加@PostConstruct 注解;@PostConstruct注解參考博文 -> 點擊前往
》指定登錄頁面、登錄表單請求url
》排除登錄頁面、登錄表單請求url的權限驗證

package com.example.wiremock.config; import com.example.wiremock.entity.properties.SecurityProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.net.SecureCacheResponse; @Configuration @Slf4j public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private SecurityProperty securityProperty; private String LOGIN_PAGE; @PostConstruct public void init() { LOGIN_PAGE = securityProperty.getBrowser().getLoginPage(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置彈出框登錄 http.formLogin() // .loginPage("/dev/xiangxu-login.html") // .loginPage("/dev/authentication/require") // 自定義登陸頁面 .loginPage(LOGIN_PAGE) .loginProcessingUrl("/dev/xiangxu/login") // 自定義登陸請求路徑(默認是:/login) .and().authorizeRequests() // 請求權限設置 // .antMatchers("/dev/xiangxu-login.html").permitAll() // .antMatchers("/dev/authentication/require").permitAll() .antMatchers(LOGIN_PAGE).permitAll() .antMatchers("/dev/xiangxu/login").permitAll() .anyRequest() // 所有請求 .authenticated() // 所有請求都進行權限驗證 .and().csrf().disable(); // 取消“跨站防護” } }
5 登錄頁面處理
需求:根據請求url的類型進行不同的登錄響應,例如 -> 如果請求url是以 .html 結尾的就直接返回登錄頁面,如果不是就直接返回JSON格式的提示信息(PS:假設請求url還未進行權限驗證)
5.1 思路
在SpingSecurity的配置類中修改登錄頁面,將登錄頁面修改成一個congroller層控制方法;在該控制方法中來判斷請求路徑的類型,然后做出不同的響應
5.2 開發步驟
5.2.1 修改SpringSecurity配置
將登錄頁面修改為一個控制層的請求路徑
技巧01:任然要排除登錄頁面的權限控制,因為最終會跳轉到登錄頁面進行登錄

package com.example.wiremock.config; import com.example.wiremock.entity.properties.SecurityProperty; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.net.SecureCacheResponse; @Configuration @Slf4j public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private SecurityProperty securityProperty; private String LOGIN_PAGE; @PostConstruct public void init() { LOGIN_PAGE = securityProperty.getBrowser().getLoginPage(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); // return new MyPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // http.httpBasic() // 配置彈出框登錄 http.formLogin() // .loginPage("/dev/xiangxu-login.html") .loginPage("/dev/authentication/require") // 自定義登陸頁面 // .loginPage(LOGIN_PAGE) .loginProcessingUrl("/dev/xiangxu/login") // 自定義登陸請求路徑(默認是:/login) .and().authorizeRequests() // 請求權限設置 // .antMatchers("/dev/xiangxu-login.html").permitAll() .antMatchers("/dev/authentication/require").permitAll() .antMatchers(LOGIN_PAGE).permitAll() .antMatchers("/dev/xiangxu/login").permitAll() .anyRequest() // 所有請求 .authenticated() // 所有請求都進行權限驗證 .and().csrf().disable(); // 取消“跨站防護” } }
5.2.2 編寫控制層邏輯
》獲取請求url信息
從請求緩存中獲取用戶的請求信息
》根據從請求緩存中獲取到的請求來判斷響應內容
如果請求url是以 .html 結尾就返回登錄頁面,反之返回JSON格式的提示信息

package com.example.wiremock.controller; import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.wiremock.entity.SimpleResponse; import com.example.wiremock.entity.properties.SecurityProperty; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController public class BrowserSecurityController { private Logger log = LoggerFactory.getLogger(getClass()); /** 請求緩存對象 */ private RequestCache requestCache = new HttpSessionRequestCache(); // 請求緩存對象,利用該對象可以獲取一些請求的緩存 /** 重定向對象 */ private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); // 頁面跳轉用的 /** 依賴注入 登錄頁面配置信息實體類 */ @Autowired private SecurityProperty securityProperty; /** * 所有進行身份驗證的請求都會被跳轉到這里 * @param request * @param response * @return * @throws IOException */ @RequestMapping(value = "/authentication/require") @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (savedRequest != null) { // 獲取請求緩存成功時 String targetUrl = savedRequest.getRedirectUrl(); // 獲取請求緩存的url log.info("引發請求的url為:" + targetUrl); if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) { // 如果緩存請求的url是以.html結尾就進行跳轉 log.info("=== ONE ==="); log.info("配置文件中的登陸頁路徑為:" + securityProperty.getBrowser().getLoginPage()); log.info("=== TWO ==="); redirectStrategy.sendRedirect(request, response, securityProperty.getBrowser().getLoginPage()); } } return new SimpleResponse("訪問的服務需要進行身份驗證,請引導用戶進行到登錄頁面。"); } }
5.2.3 測試
》非 .html 結尾的請求
》.html 結尾的請求
只要是 .html 結尾的都行
技巧01:請求發出后會自動跳轉到登錄頁面
bug01:跳轉登錄頁面進行登錄操作,登錄成功后就會跳轉到之前的請求中;我們現在的項目一般都是基於RestfulAPI的前后端分離項目,我們要求權限驗證成功后直接返回一個JSON格式的信息就行啦,該問題待解決:詳情參見3.2.4 登錄成功后響應信息
6 響應格式處理
SpringSecurity默認登錄成功后會跳轉到之前的請求路徑,登錄失敗后跳轉到 /error;開發者可以將定義響應的數據格式為JSON格式
需求:根據用戶在properties文件中的配置來決定登錄后的響應格式
技巧01:以JSON格式響應 -> 請參見3.2.4 登錄成功后響應信息
6.1 創建響應類型枚舉
該枚舉主要用來指定登錄后的響應數據類型

package com.example.wiremock.enums; /** * @author 王楊帥 * @create 2018-05-13 17:23 * @desc **/ public enum LoginType { REDIRECT, JSON }
6.2 Properties自定義配置實體類
技巧01:給BrowserProperty的loginType屬性設置了默認值,用來指明默認的登錄后響應類型是JSON類型

package com.example.wiremock.entity.properties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @author 王楊帥 * @create 2018-05-13 13:45 * @desc **/ @ConfigurationProperties(prefix = "xiangxu.security") public class SecurityProperty { private BrowserProperty browser = new BrowserProperty(); public BrowserProperty getBrowser() { return browser; } public void setBrowser(BrowserProperty browser) { this.browser = browser; } }

package com.example.wiremock.entity.properties; import com.example.wiremock.enums.LoginType; /** * @author 王楊帥 * @create 2018-05-13 13:46 * @desc **/ public class BrowserProperty { private String loginPage = "/dev/xiangxu-login.html"; private LoginType loginType = LoginType.JSON; public String getLoginPage() { return loginPage; } public void setLoginPage(String loginPage) { this.loginPage = loginPage; } public LoginType getLoginType() { return loginType; } public void setLoginType(LoginType loginType) { this.loginType = loginType; } }
6.3 重寫登錄成功后的兩個處理類
6.3.1 XiangXuAuthenticationSuccessHandler
不在是實現AuthenticationSuccessHandler接口,而是繼承SavedRequestAwareAuthenticationSuccessHandler父類

package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.wiremock.entity.properties.SecurityProperty; import com.example.wiremock.enums.LoginType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component //public class XiangXuAuthenticationSuccessHandler implements AuthenticationSuccessHandler { public class XiangXuAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { private Logger log = LoggerFactory.getLogger(getClass()); /** json數據轉化對象 */ @Autowired private ObjectMapper objectMapper; /** 自定義配置類對象 */ @Autowired private SecurityProperty securityProperty; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("登陸成功"); // 登錄后的形影數據格式判斷 if (LoginType.JSON.equals(securityProperty.getBrowser().getLoginType())) { response.setContentType("application/json;charset=UTF-8"); // 響應類型 response.getWriter().write(objectMapper.writeValueAsString(authentication)); // 數據轉化成json類型后再進行響應操作 } else { super.onAuthenticationSuccess(request, response, authentication); } } }
6.3.2 XiangXuAuthenticationFailureHandler
不在是實現AuthenticationFailureHandler接口,而是繼承SimpleUrlAuthenticationFailureHandler父類

package com.example.wiremock.authentication; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.wiremock.entity.properties.SecurityProperty; import com.example.wiremock.enums.LoginType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component //public class XiangXuAuthenticationFailureHandler implements AuthenticationFailureHandler { public class XiangXuAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private Logger log = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper ObjectMapper; /** 自定義配置類對象 */ @Autowired private SecurityProperty securityProperty; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { log.info("登陸失敗"); // 登錄后響應數據格式判斷 if (LoginType.JSON.equals(securityProperty.getBrowser().getLoginType())) { // JSON格式返回 response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(ObjectMapper.writeValueAsString(exception));; } else { // HTML跳轉 super.onAuthenticationFailure(request, response, exception); } } }
6.3.3 properties配置
#security.basic.enabled = false
xiangxu.security.browser.loginPage = /dev/xiangxu-login.html
xiangxu.security.browser.loginType = REDIRECT
https://cloud.tencent.com/developer/support-plan?invite_code=xtjeour7cozu
·下面是我的公眾號二維碼,歡迎關注·
尋渝記
微信號:xyj_fury