項目開發離不開認證授權,簡單來說,認證解決你是誰的問題,授權解決你能干什么的問題。下面講講SpringSecurity的授權。
一、授權基本知識
1、授權因項目而異
一些業務系統,如電商網站,只需區分是否登錄,或者是普通用戶還是VIP用戶等基本角色,它們的權限基本不會改變,這種情況可以直接在代碼中寫死。還有一些內管系統,如運營人員管理系統 ,角色眾多,權限復雜,權限規則隨着公司和業務的發展不斷變化,這種情況必須配置權限。
2、什么是授權
授權不是說在頁面上隱藏某個連接或者按鈕就完事兒,而是要判斷當前用戶有沒有訪問該連接的權限。授權模型中要明確兩個要素:系統配置信息和用戶權限信息。系統配置信息中記錄了url連接和每一個連接需要的權限,比如www.a.com/user需要A權限;用戶權限信息則記錄了某用戶具有的權限,比如用戶張三具有A、B、C權限。當一個用戶發了一個連接請求,系統會拿這兩份信息比對,如果這個請求需要A權限,發請求的這個用戶也有A權限,那么就可以訪問。
二、SpringSecurity中的授權
1、授權之是否需要登錄
有一些鏈接需要登錄才能訪問,也有一些鏈接無需登錄就能訪問,怎么允許鏈接不登錄就能訪問呢?SpringSecurity配置如下:
@EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") http.authorizeRequests() .antMatchers( "/login.html", "/authentication/require", "/code/image", "/session/invalid", "/logout.html" ).permitAll()//不需要身份認證 .anyRequest() .authenticated(); http.csrf().disable(); } }
2、授權之區分簡單角色
鏈接"/user"需要"ADMIN"權限才能訪問,配置如下:
@EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") http.authorizeRequests() .antMatchers( "/login.html", "/authentication/require", "/code/image", "/session/invalid", "/logout.html" ).permitAll() .antMatchers("/user").hasRole("ADMIN") .anyRequest() .authenticated(); http.csrf().disable(); } }
@Component public class MyUserDetailsService implements UserDetailsService{ @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
說明:
a)如上配置,瀏覽器訪問http://localhost/user,報403無權訪問;訪問http://localhost/user/1,可以獲取到數據。
b)將MyUserDetailsService中“admin”改為“ROLE_ADMIN”,http://localhost/user或者http://localhost/user/1都可以獲取到數據。
c)注意,hasRole("ADMIN")對應的是“ROLE_ADMIN”,大小寫敏感。必須有“ROLE_”。
d)antMatchers中支持通配符,如下配置,則http://localhost/user/或者http://localhost/user/1都無權訪問。


3、授權之權限表達式
上面講了permitAll和hasRole,SpringSecurity還有一些其他的表達式如下:

說明:
a)anonymous是匿名,即不登錄時可以訪問。
b).antMatchers("/user").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')") ;ADMIN權限和IP地址同時滿足。
c)判斷hasRole中的權限時,在設置權限時添加前綴ROLE_,其它表達式時不需要添加。
4、將授權提取到AuthorizeConfigProvider中統一管理
從上面的示例中看到授權代碼寫在WebSecurityConfigurerAdapter中,耦合度高,下面解決這個問題
1)AuthorizeConfigProvider.java
public interface AuthorizeConfigProvider { void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config); }
2)MyAuthorizeConfigProvider.java,配置通用url
@Component public class MyAuthorizeConfigProvider implements AuthorizeConfigProvider { @Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) { config.antMatchers( "/login.html", "/authentication/require", "/code/image", "/session/invalid", "/logout.html" ).permitAll(); } }
3)DempAuthorizeConfigProvider.java,配置自定義url
@Component public class DempAuthorizeConfigProvider implements AuthorizeConfigProvider { @Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) { config.antMatchers("/user", "/demo.html" ).hasRole("ADMIN"); } }
4)AuthorizeConfigManager.java
public interface AuthorizeConfigManager { void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config); }
5)MyAuthorizeConfigManager.java
@Component public class MyAuthorizeConfigManager implements AuthorizeConfigManager { //Spring啟動的時候,所有AuthorizeConfigProvider的接口的實現都(自動)放在AuthorizeConfigProviders中 @Autowired private Set<AuthorizeConfigProvider> AuthorizeConfigProviders; @Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) { for(AuthorizeConfigProvider authorizeConfigProvider:AuthorizeConfigProviders) { authorizeConfigProvider.config(config); } config.anyRequest().authenticated();//所有請求需要身份認證 } }
上面的代碼實現了權限代碼的解耦,現在看一下WebSecurityConfigurerAdapter中的配置:
@EnableWebSecurity @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ ... ... @Autowired private AuthorizeConfigManager authorizeConfigManager; @Override protected void configure(HttpSecurity http) throws Exception { ... ... /**http.authorizeRequests() .antMatchers( "/login.html", "/authentication/require", "/code/image", "/session/invalid", "/logout.html" ).permitAll()//該路徑不需要身份認證 .antMatchers("/user").hasRole("ADMIN") //.antMatchers(HttpMethod.GET,"/user/*").hasRole("ADMIN")//GET請求需要這個權限 //.antMatchers("/user").access("hasRole('ADMIN') and hasIpAddress('192.168.1.0/24')") .anyRequest() .authenticated();*/ http.csrf().disable();//先禁止掉跨站請求偽造防護功能 authorizeConfigManager.config(http.authorizeRequests());//將授權代碼提取到AuthorizeConfigManager中 } }
測試,訪問http://localhost/user,http://localhost/demo.html報403,上述配置生效。
5、通用RBAC(Role-Based Access Control)數據模型
上面的授權都是靜態的,即都是寫死在代碼中的,遇到復雜的權限管理,都是配置在數據庫中的,一般由五張表組成,即RBAC。
1)RBAC數據模型的五張表
用戶表,存儲用戶信息,由業務人員維護;
角色表,存儲角色信息,由業務人員維護 ;
資源表,存儲資源信息(菜單、按鈕及其URL),由開發人員維護;
用戶-角色關系表,存儲用戶和角色的對應關系,多對多,由業務人員維護;
角色-資源關系表,存儲角色和資源的對應關系 ,多對多,由業務人員維護;
2)代碼實現
RbacService.java
public interface RbacService { boolean hasPermission(HttpServletRequest request,Authentication authentication); }
RbacServiceImpl.java
@Component("rbacService")
public class RbacServiceImpl implements RbacService{
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
System.out.println("---------進入hasPermission方法:url"+request.getRequestURI());
Object principal = authentication.getPrincipal();
boolean hasPermission = false;
if(principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
//根據username,讀取用戶所擁有權限的所有url
Set<String> urls = new HashSet<>();
urls.add("/user");
urls.add("/index.html");
for(String url:urls) {
if(antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
修改DempAuthorizeConfigProvider.java
@Component @Order(Integer.MAX_VALUE)//指定順序,最后讀取 public class DempAuthorizeConfigProvider implements AuthorizeConfigProvider {
@Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) { /**config.antMatchers("/user", "/demo.html" ).hasRole("ADMIN"); */ config.anyRequest().access("@rbacService.hasPermission(request,authentication)"); //anyRequest放到最后讀,注釋掉MyAuthorizeConfigManager中的anyRequest,否則會覆蓋此處的anyRequest } }
修改MyAuthorizeConfigProvider.java
@Component @Order(Integer.MIN_VALUE)//指定順序,先讀取 public class MyAuthorizeConfigProvider implements AuthorizeConfigProvider { @Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) { config.antMatchers( "/login.html", "/authentication/require", "/code/image", "/session/invalid", "/logout.html" ).permitAll(); } }
修改MyAuthorizeConfigManager.java
@Component public class MyAuthorizeConfigManager implements AuthorizeConfigManager { //Spring啟動的時候,所有AuthorizeConfigProvider的接口的實現都(自動)放在AuthorizeConfigProviders中 @Autowired //private Set<AuthorizeConfigProvider> AuthorizeConfigProviders; private List<AuthorizeConfigProvider> AuthorizeConfigProviders;//改為有序集合 @Override public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) { for(AuthorizeConfigProvider authorizeConfigProvider:AuthorizeConfigProviders) { authorizeConfigProvider.config(config); } //config.anyRequest().authenticated();//所有請求需要身份認證,注銷掉,因為DempAuthorizeConfigProvider中有anyRequest了 } }
說明:
a)通過設置@Order注解,使通用url先讀取,首選執行permitAll;自定義url后讀取,執行rbacService.hasPermission方法。
b)DempAuthorizeConfigProvider中指定了anyRequest訪問hasPermission方法,MyAuthorizeConfigManager中去掉最后一句。
c)MyAuthorizeConfigManager中改為有序集合List,先實現AuthorizeConfigProvider的bean先被讀取,與@Order對應。
測試:
http://localhost/login.html可以訪問,輸入用戶名、密碼登錄。
登錄后訪問http://localhost/user或者http://localhost/index.html,有權限,因為hasPermission方法中設置了。
登錄后訪問http://localhost/user/1或者http://localhost/demo.html,報403無權限。
