项目开发离不开认证授权,简单来说,认证解决你是谁的问题,授权解决你能干什么的问题。下面讲讲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无权限。