javascript
Spring Boot实践 | 利用Spring Security快速搞定权限控制
目錄
開始之前
快速開始
使用內(nèi)存簽名服務
使用數(shù)據(jù)庫簽名服務
使用自定義簽名服務
限制請求
強制使用HTTPS
防止跨站點偽造請求
用戶認證功能
在java web工程中,一般使用Servlet過濾器(Filter)對于請求進行攔截,然后在Filter中通過自己的驗證邏輯來決定是否放行請求。基于這一原理,常用的SpringMVC實現(xiàn)了自己的攔截器,同樣的,Spring Security也是基于這個原理,在進入到DispatcherServlet前就可以對Spring MVC的請求進行攔截,然后通過一定的驗證(一般驗證用戶是否有某個權限、請求類型、請求方式等),從而決定是否放行某個請求。
開始之前
?1、Spring Security的主要功能就是通過一定的驗證,從而決定是否放行某個請求,可以實現(xiàn)用戶訪問權限控制,HTTP和HTTPS訪問控制、CSRF(跨站點請求偽造)訪問控制等;
2、Spring Security的攔截會默認先于其它過濾器之前執(zhí)行;
3、針對Spring Security,角色權限的命名強制都以"ROLE_"開頭且全部大寫,比如"ROLE_USER","ROLE_ADMIN",
"ROLE_DBA",當然,角色權限可以任意命名,甚至可以定義一個角色權限為"ROLE_HAHA","ROLE_WUDI"。其提供的方法有的會自動給角色權限加上"ROLE_",這時,就不能給角色權限加上"ROLE_",這里需要注意(后面會提供說明)。
快速開始
1、在Spring Boot項目中引入Spring Security的依賴。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> </dependency>2、啟動注解
官方定義的是:web工程使用@EnableWebSecurity,非web工程可以使用@EnableGlobalAuthentication。而事實上,@EnableWebSecurity已經(jīng)標注了@EnableGlobalAuthentication(可以點進源碼查看)。
所以,只需要在啟動類上標注@EnableWebSecurity即可開啟Spring Security功能。
3、啟動Spring Boot項目
啟動Spring Boot項目后,可以在Console里面看到隨機生成的秘鑰,如下。
隨即在瀏覽器中任意輸入一個存在于Spring Boot項目中的url,就會出現(xiàn)一個攔截頁面,輸入剛剛生成的秘鑰即可訪問到預期的頁面,如下。
4、短板
上述過程暴露了一下問題:
-每次啟動都會生成不同的秘鑰,造成在訪問的過程每次都要輸入不同的秘鑰,如果丟失又要重啟,實在是不方便;
-用戶只能使用‘user’賬號,無法多樣化,不適合構建不同的權限;
-不能自定義自己的驗證方式和策略;
-驗證界面不美觀;
-不能定義哪些url需要驗證,哪些不需要;
.....................
為了克服只能使用user+自動生成的秘鑰引起的弊端,Spring Security提供了使用內(nèi)存簽名服務、數(shù)據(jù)庫簽名服務和自定義簽名服務。
使用內(nèi)存簽名服務
顧名思義,就是將用戶信息存放在內(nèi)存中(實際項目都是在數(shù)據(jù)庫中)。相對而言,它比較簡單,適合于快速搭建測試環(huán)境。
1、繼承WebSecurityConfigurerAdapter重寫攔截配置
里面有三個常用方法供我們重寫,如下。
@Configuration public class RoleConfig extends WebSecurityConfigurerAdapter {/*** <p>用來配置用戶簽名服務,主要是user-detail機制,還可以給與用戶賦予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// TODO Auto-generated method stubsuper.configure(auth);}/*** <p>用來配置Filter鏈* */@Overridepublic void configure(WebSecurity web) throws Exception {// TODO Auto-generated method stubsuper.configure(web);}/*** <p>用來配置攔截保護請求,比如什么請求放行,什么請求需要驗證* */@Overrideprotected void configure(HttpSecurity http) throws Exception {// TODO Auto-generated method stubsuper.configure(http);} }2、定義簽名服務
根據(jù)上面所述,我們只需要重寫下面的方法即可。
/*** <p>用來配置用戶簽名服務,主要是user-detail機制,還可以給與用戶賦予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// TODO Auto-generated method stubsuper.configure(auth);}完整的示例
@Configuration public class RoleConfig extends WebSecurityConfigurerAdapter {/*** <p>用來配置用戶簽名服務,主要是user-detail機制,還可以給與用戶賦予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密碼編碼器-在Spring5的Security中都要求使用密碼編碼器,否則會發(fā)生異常BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();//使用內(nèi)存簽名服務auth.inMemoryAuthentication()//設置密碼編碼器.passwordEncoder(passwordEncoder)//注冊用戶admin,密碼為abc123,并且賦予USER和ADMIN的角色權限 .withUser("admin").password(passwordEncoder.encode("abc123")).roles("USER", "ADMIN")//連接方法and().and()//注冊用戶myuser,密碼為123456,并且賦予USER角色權限 .withUser("myuser").password(passwordEncoder.encode("123456")).roles("USER");//還需要注冊其它用戶繼續(xù)用and()連接......} }3、重新啟動Spring Boot項目
可以發(fā)現(xiàn)Console中不再為我們打印秘鑰了,隨即在瀏覽器中輸入任意一個Controller中存在的url,同樣出現(xiàn)攔截頁面,輸入我們自定義注冊的用戶即可訪問到預期的頁面。
4、其它
user-detail機制其它構造方法
| 方法 | 描述 |
| accountExpired(boolean accountExpired) | 設置賬號是否過期 |
| accountLocked(boolean accountLocked) | 是否鎖定賬號 |
| credentialsExpired(boolean credentialsExpired) | 定義憑證是否過期 |
| disabled(boolean disabled) | 是否禁用用戶 |
| username(String username) | 定義用戶名,不能為null |
| authorities(GrantedAuthority... authorities) | 賦予一個或多個權限,需要加上ROLE_ |
| authorities(List<? extends GrantedAuthority> authorities) | 使用列表(List)賦予權限 |
| password(String password) | 定義密碼 |
| roles(String... roles) | 賦予一個或多個權限,會自動加上ROLE_ |
提示:上面示例已經(jīng)演示了username()、password()和roles()構造方法了,其它的依葫蘆畫瓢即可。?
使用數(shù)據(jù)庫簽名服務
畢竟內(nèi)存有限,且也不適合實際開發(fā)環(huán)境。因此Spring Security提供了對數(shù)據(jù)庫的查詢方法來滿足需求。
1、創(chuàng)建數(shù)如下?lián)?并配置好數(shù)據(jù)源等其他配置
說明:user_available字段表示用戶是否可用1-可用,0-不可用。
2、繼承WebSecurityConfigurerAdapter重寫攔截配置,定義簽名服務
@Configuration public class RoleConfig2 extends WebSecurityConfigurerAdapter {//注入數(shù)據(jù)源@Autowiredprivate DataSource dataSource;//根據(jù)用戶名查詢用戶信息private final String getUserByUsername = "SELECT user_name as username, user_pwd as password, user_available as enabled "+ "FROM tb_user "+ "WHERE user_name = ?";//根據(jù)用戶名查詢角色信息private final String getRoleByUsername = "SELECT u.user_name as username, r.role_name as authority "+ "FROM tb_user u, tb_user_role_mid ur, tb_role r "+ "WHERE u.user_id = ur.user_id AND r.role_id = ur.role_id AND u.user_name = ?";/*** <p>用來配置用戶簽名服務,主要是user-detail機制,還可以給與用戶賦予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密碼編碼器-在Spring5的Security中都要求使用密碼編碼器,否則會發(fā)生異常BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();//使用數(shù)據(jù)庫簽名服務auth.jdbcAuthentication()//設置密碼編碼器.passwordEncoder(passwordEncoder)//數(shù)據(jù)源.dataSource(dataSource)//查詢用戶,自動判斷密碼是否一致.usersByUsernameQuery(getUserByUsername)//賦予權限.authoritiesByUsernameQuery(getRoleByUsername);} }3、重新啟動Spring Boot項目
可以發(fā)現(xiàn)Console中不再為我們打印秘鑰了,隨即在瀏覽器中輸入任意一個Controller中存在的url,同樣出現(xiàn)攔截頁面,輸入我們自定義注冊的用戶即可訪問到預期的頁面。
使用自定義簽名服務
接著使用上面的數(shù)據(jù)庫表。
1、實現(xiàn)UserDetaisService接口定義簽名服務
//這里直接標記為@Service,作為bean掃描進IoC,省去bean的單獨配置 @Service public class UserDetailsServiceImpl implements UserDetailsService {//普通點的dao層接口,獲取相應的用戶數(shù)據(jù)和角色權限@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//獲取數(shù)據(jù)庫用戶信息RoleUser user = userMapper.getUserByName(username);//獲取數(shù)據(jù)庫角色信息List<Role> roles = userMapper.listRolesByUserName(username);//將信息轉換為UserDetails對象//權限列表List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();//賦予查詢到的角色for(Role role : roles) {SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getRoleName());authorities.add(authority);}//創(chuàng)建UserDetails對象,設置用戶名、密碼和權限 這里的編碼器要保持一致BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();UserDetails userDetails = new User(user.getUserName(), passwordEncoder.encode(user.getUserPwd()), authorities);return userDetails;} } @Configuration public class RoleConfig3 extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//密碼編碼器-在Spring5的Security中都要求使用密碼編碼器,否則會發(fā)生異常BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);} }2、重新啟動Spring Boot項目
可以發(fā)現(xiàn)Console中不再為我們打印秘鑰了,隨即在瀏覽器中輸入任意一個Controller中存在的url,同樣出現(xiàn)攔截頁面,輸入我們自定義注冊的用戶即可訪問到預期的頁面。
3、小結
可以發(fā)現(xiàn),實現(xiàn)UserDetailsService這種方式最靈活,用戶數(shù)據(jù)不僅可以來源于數(shù)據(jù)庫,還可以在數(shù)據(jù)庫壓力大的情況下轉而訪問緩存獲取用戶數(shù)據(jù)。
限制請求
上面只是實現(xiàn)了驗證用戶,并且賦予了用戶某些角色權限,任何技術如果不是用于解決生產(chǎn)問題,那就是耍流氓。
前面說到,繼承WebSecurityConfigurerAdapter?經(jīng)常使用的三個需要重新方法是
@Configuration public class RoleConfig extends WebSecurityConfigurerAdapter {/*** <p>用來配置用戶簽名服務,主要是user-detail機制,還可以給與用戶賦予角色* */@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// TODO Auto-generated method stubsuper.configure(auth);}/*** <p>用來配置Filter鏈* */@Overridepublic void configure(WebSecurity web) throws Exception {// TODO Auto-generated method stubsuper.configure(web);}/*** <p>用來配置攔截保護請求,比如什么請求放行,什么請求需要驗證* */@Overrideprotected void configure(HttpSecurity http) throws Exception {// TODO Auto-generated method stubsuper.configure(http);} }前面重寫了configure(AuthenticationManagerBuilder auth)實現(xiàn)了用戶驗證和賦予用戶角色權限。接下來,就需要重寫configure(HttpSecurity http)實現(xiàn)限制請求,如下。
1、重寫configure(HttpSecurity http)方法
@Overrideprotected void configure(HttpSecurity http) throws Exception {//限定簽名后的權限http./**第一段**/authorizeRequests()//限定"/user/welcome"請求賦予ROLE_USER 或者 ROLE_ADMIN.antMatchers("/user/welcome", "/user/details").hasAnyRole("USER", "ADMIN")//限定"/admin/**"下所有請求權限賦予ROLE_ADMIN.antMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN")//其它路徑允許簽名后訪問.anyRequest().permitAll()/**第二段**///對于沒有配置權限的其它請求允許匿名訪問.and().anonymous()//使用Spring Security的默認登錄頁面.and().formLogin()//HTTP基礎驗證.and().httpBasic();}2、現(xiàn)身說法
上面的配置,擁有ADMIN權限的可以訪問"/admin/**"路徑下的所有,擁有USER權限或ADMIN的可以訪問"/user/welcome"和"/user/details",其它路徑可以匿名訪問。
對于這里的限制請求配置,明顯產(chǎn)生了權限沖突,針對此問題,Spring Security采取了配置優(yōu)先原則來解決,比如上面第二段允許匿名的訪問,且沒有給出uri地址,但是第一段中加入了限制,還是會采取第一段的限制訪問。
因此,生產(chǎn)中,需要把具體的配置防止前面,把不具體的配置放到后面。
還應該注意方法上是否需要加上"ROLE_"的區(qū)別。
另外,除了使用上面的Ant風格編碼,還可以使用正則規(guī)則,例如
http.authorizeRequests().regexMatchers("/user/welcome", "/user/details").hasAnyRole("USER", "ADMIN").regexMatchers("/admin/.*").hasAnyAuthority("ROLE_ADMIN").and().formLogin().and().httpBasic();3、其它權限方法說明
| 方法 | 說明 |
| access(String) | 參數(shù)為Spring EL,如果返回true則允許訪問,需要配合Spring EL表達式使用 |
| anonymous() | 允許匿名訪問 |
| authorizeRequests() | 限定通過簽名的請求 |
| anyRequest() | 限定任意的請求 |
| hasAnyRole(String ...) | 將訪問權限賦予多個角色(角色會自動加入前綴"ROLE_") |
| hasRole(String) | 將訪問權限賦予一個角色(角色會自動加入前綴"ROLE_") |
| permitAll() | 無條件允許訪問 |
| and() | 連接詞,并取消之前限定的前提規(guī)則 |
| httpBasic() | 啟用瀏覽器的HTTP基礎驗證 |
| formLogin() | 啟用Spring Security默認的登錄頁面 |
| not() | 對其它方法的訪問采取求反 |
| fullyAuthorized() | 如果是完整驗證(并發(fā)Remember-me),則允許訪問 |
| denyAll() | 無條件不允許任何訪問 |
| hasIpAddress(String) | 如果是給定的IP地址則允許訪問 |
| rememberme() | 用戶通過Remember-me功能驗證就允許訪問 |
| hasAnyAuthority(String ...) | 如果是給定的多個角色就允許訪問(角色不會自動加入前綴"ROLE_") |
| hasAuthority(String) | 如果是給定的一個角色就允許訪問(角色不會自動加入前綴"ROLE_") |
4、使用Spring EL表達式配置訪問權限(等價于Ant風格配置)
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests()//使用Spring EL 表達式現(xiàn)在只有角色ROLE_USER或者ADMIN.antMatchers("/user/**").access("hasRole('USER') or hasRole('ADMIN')")//設置訪問權限給角色ADMIN,要求是完整登錄(非記住我登錄).antMatchers("/admin/welcome1").access("hasAuthority('ROLE_ADMIN') && isFullyAuthenticated()")//設置訪問權限給角色ADMIN,允許使用非完整登錄(使用記住我登錄).antMatchers("/admin/welcome2").access("hasAuthority('ROLE_ADMIN')")//使用記住我功能.and().rememberMe()//Spring Security默認登錄頁面.and().formLogin()//http基礎驗證.and().httpBasic(); }上面的代碼中,在access()方法里用了3個Spring表達式,除此之外,Spring Security還提供了以下表達式方法:
| 方法 | 說明 |
| authentication() | 用戶認證對象 |
| denyAll() | 拒絕任何訪問 |
| hasAnyRole(String ...) | 賦予一個或多個角色權限,會自動加上"ROLE_" |
| hasRole(String) | 賦予一個角色權限,會自動加上"ROLE_" |
| hasIpAdress(String) | 是否請求來自指定IP |
| isAnonymous() | 是否匿名訪問 |
| isAuthenticated() | 是否用戶通過認證簽名 |
| isFullyAuthenticated() | 限制完整登錄(非記住我功能) |
| isRememberMe() | 是否通過"記住我"功能通過驗證 |
| hasAuthority(String) | 賦予一個角色權限,需要手動加上"ROLE_" |
| hasAnyAuthority(String ...) | 賦予一個或多個角色權限,需要手動加上"ROLE_" |
| permitAll() | 無條件允許任何訪問 |
| principal() | 用戶的principal對象 |
強制使用HTTPS
1、簡單的示例
@Overrideprotected void configure(HttpSecurity http) throws Exception {http//使用安全渠道,強制使用https.requiresChannel().antMatchers("/admin/**").requiresSecure().and()//不使用https.requiresChannel().antMatchers("/user/**").requiresInsecure().and()//限定角色訪問權限.authorizeRequests().antMatchers("/admin/**").hasAnyRole("ADMIN").antMatchers("/user/**").hasAnyRole("ROLE", "ADMIN"); }2、現(xiàn)身說法
這里的requiresChannel()方法說明使用通道,然后antMatchers()是一個限定請求,然后requiresSecure()表示使用HTTPS請求,而requiresInsecure()則是取消安全請求的限制,這樣就可以使用普通的HTTP。
防止跨站點偽造請求
Spring Security在默認的情況下,是已經(jīng)開啟了防止CSRF攻擊的過濾器,如果需要關閉(當然是不建議),可以這么做:
http.csrf().disable().authorizeRequests()...Spring Security會為每次需要提交的表單提供一個key-value形式的參數(shù),這個信息不存在Cookie中,所以無法偽造,然后根據(jù)客戶端傳遞的參數(shù)和服務端比較,正確才會放行。
用戶認證功能
1、自定義登錄頁面并使用"記住我"功能
簡單的示例
http//設置角色訪問權限.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").and()//啟用remember me功能.rememberMe().tokenValiditySeconds(86400).key("remember-me-key").and()//啟用HTTP Basic功能.httpBasic().and()//通過簽名后可以訪問任何路徑.authorizeRequests().antMatchers("/**").permitAll().and()//設置默認的登錄頁和跳轉路徑.formLogin().loginPage("/login/page").defaultSuccessUrl("/admin/welcome1");現(xiàn)身說法
這里的?rememberMe()方法就是啟用"記住我"功能,有效時間為86400s(即1天),在瀏覽器中以Cookie存儲,鍵"remember-me-key"。loginPage()方法設置默認的登錄頁,defaultSuccessUrl()方法設置默認的跳轉路徑。
這里的"/login/page"所映射的路徑,可以使用傳統(tǒng)的Controller控制層去映射,也可以使用新增映射關系去完成,如下:
@Configuration public class WebConfig implements WebMvcConfigurer {/*** <p>新增映射關系* */@Overridepublic void addViewControllers(ViewControllerRegistry registry) {//使得/login/page映射為login.jspregistry.addViewController("/login/page").setViewName("login");//使得/logout/page映射為logout_welcome.jspregistry.addViewController("/logout/page").setViewName("logout_welcome");//使得/logout映射為logout.jspregistry.addViewController("/logout").setViewName("logout");} }注意
登錄頁面的參數(shù)名必須是賬號:username、密碼:password、記住我:remember-me,且記住我為一個checkbox,這樣Spring Security才能獲取這些參數(shù),且提交方式必須為POST。
2、啟用HTTP Basic認證
在前面調(diào)用httpBasic()方法就是啟動了HTTP Basic功能。還可以調(diào)用realmName(String)方法為認證設置模態(tài)對話框的標題。
3、登出
默認情況下,Spring Security會提供一個URI--"/logout",用POST請求了這個uri,Spring Security就會推出,且清楚remember me的相關信息。仿造自定義登錄,也可以實現(xiàn)自定義推出。
總結
以上是生活随笔為你收集整理的Spring Boot实践 | 利用Spring Security快速搞定权限控制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 清华大学提出基于生成对抗神经网络的自然
- 下一篇: 如何高效管理自己的电脑?文件再多也不乱!