使用Spring Security控制授权
in JavaDevelop with 0 comment

使用Spring Security控制授权

in JavaDevelop with 0 comment

Spring Security授权简介

授权又是什么概念呢?

现在来回顾下安全的概念:

  1. 你是谁?
  2. 你能干什么?

前面讲解的全是认证,也就是解决你是谁的问题;

这章讲解你能干什么的问题。很多人叫权限控制,鉴权,授权等;最终的核心目的都是一样的,
控制这个用户能在系统中干什么?

security对授权的定义

上图意思就是说,页面能看到的只是体验和ui交互问题;而对应后台某一个url的是否能被访问被认为是权限

权限场景分析

两个系统的权限特点是不一样的。不应该部署在一个应用中

当权限比较简单的时候,也就是对应业务系统来说这种需求;

security就支持了,可以把规则写在代码中进行控制

security的权限控制

之前其实已经写过

// 对请求授权配置:注意方法名的含义
 .authorizeRequests()
 .antMatchers(
         SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
         SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
         securityProperties.getBrowser().getLoginPage(),
         SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*", // 图形验证码接口
         securityProperties.getBrowser().getSignUpUrl(),  // 注册页面
         securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".json",
         securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".html",
         "/user/regist", // 注册请求,后面会介绍怎么把这个只有使用方知道放行的配置剥离处理
         "/error",
         "/connect/*",
         "/auth/*",
         "/signin"
 )
  // 放行以上路径
 .permitAll()
 // 该路径,只允许有 ADMIN 角色的人访问
 .antMatchers("/user").hasRole("ADMIN")
 .anyRequest()
 // 对任意请求都必须是已认证才能访问
 .authenticated()

这个时候再访问系统,登录后发现 /user 不能访问了;访问被拒绝了,那么就是因为当前登录的用户没有 ADMIN这个角色

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Sun Aug 12 17:23:47 CST 2018
There was an unexpected error (type=Forbidden, status=403).
Forbidden

如何给用户分配角色

就是在我们之前实现的UserDetailsService来进行构建的用户信息

com.example.demo.security.MyUserDetailsService

private SocialUser getUserDetails(String username) {
    String password = passwordEncoder.encode("123456");
    logger.info("数据库密码{}", password);
    SocialUser admin = new SocialUser(username,
//                              "{noop}123456",
                                      password,
                                      true, true, true, true,
                                      AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"));
    return admin;
}

这里的角色需要添加 ROLE 前缀;因为之前使用的是 hasRole ; 这个在下一章节讲解是为什么;

怎么匹配restfull的url

还是之前那个方法配置,可以传递请求和通配符

.antMatchers(HttpMethod.GET, "/user/*").hasRole("ADMIN")

在权限规则简单的情况下,就可以使用这里的知识进行构建权限系统。

Spring Security源码解析

spring security的基本原理之前讲解过了。这章主要看后面两个:

AnonymousAuthenticationFilter

AnonymousAuthenticationFilter : 匿名过滤器,位置固定,前面所有的都走完之后,会经过该过滤器

该过滤器之前自己也是备受折磨,特别是在调试oath2的时候,在没有正确开始basic登录的时候,
就一直在抱怨,为什么会走这个呢?

看看最主要的源码:

public AnonymousAuthenticationFilter(String key) {
  // 固定了用户信息和角色
  this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}

public AnonymousAuthenticationFilter(String key, Object principal,
    List<GrantedAuthority> authorities) {
  Assert.hasLength(key, "key cannot be null or empty");
  Assert.notNull(principal, "Anonymous authentication principal must be set");
  Assert.notNull(authorities, "Anonymous authorities must be set");
  this.key = key;
  this.principal = principal;  // 用户信息不是一个对象,是一个字符串
  this.authorities = authorities;
}

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {

  // 如果之前没有身份认证
  // 则创建一个 AnonymousAuthenticationToken
  // 也就是说到达 FilterSecurityInterceptor 中的时候一定有一个身份信息
  if (SecurityContextHolder.getContext().getAuthentication() == null) {
    SecurityContextHolder.getContext().setAuthentication(
        createAuthentication((HttpServletRequest) req));

    if (logger.isDebugEnabled()) {
      logger.debug("Populated SecurityContextHolder with anonymous token: '"
          + SecurityContextHolder.getContext().getAuthentication() + "'");
    }
  }
  else {
    if (logger.isDebugEnabled()) {
      logger.debug("SecurityContextHolder not populated with anonymous token, as it already contained: '"
          + SecurityContextHolder.getContext().getAuthentication() + "'");
    }
  }

  chain.doFilter(req, res);
}

protected Authentication createAuthentication(HttpServletRequest request) {
  AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
      principal, authorities);
  auth.setDetails(authenticationDetailsSource.buildDetails(request));

  return auth;
}

FilterSecurityInterceptor

看源码技巧:找准一个关键入口,然后分析调用关系和类图

主流程:拿到 配置信息,用户身份信息,请求信息 然后给投票者进行投票;最后根据策略进行决定是否放行

从两个流程进行分析:

  1. 未登录访问被拦截
  2. 登录后访问

未登录访问被拦截

访问: http://localhost:8080/user/1

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
		Filter {

  public void doFilter(ServletRequest request, ServletResponse response,
    FilterChain chain) throws IOException, ServletException {
    // 拿到请求和响应封装成一个 invocation(调用)
    FilterInvocation fi = new FilterInvocation(request, response, chain);
    invoke(fi);
  }
  public void invoke(FilterInvocation fi) throws IOException, ServletException {
    // 保证每次请求只被检查一次,依据就是 request中的 FILTER_APPLIED
		if ((fi.getRequest() != null)
				&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
				&& observeOncePerRequest) {
			// filter already applied to this request and user wants us to observe
			// once-per-request handling, so don't re-do security checking
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		else {
      // 如果是第一次经过该过滤器,则设置标记
			// first time this request being called, so perform security checking
			if (fi.getRequest() != null && observeOncePerRequest) {
				fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
			}
      // 权限的校验
			InterceptorStatusToken token = super.beforeInvocation(fi);

      // 如果校验通过则是执行真正的服务了
			try {
				fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
			}
			finally {
				super.finallyInvocation(token);
			}

			super.afterInvocation(token, null);
		}
	}
}

// 抽象父类
public abstract class AbstractSecurityInterceptor implements InitializingBean,
		ApplicationEventPublisherAware, MessageSourceAware {
      protected InterceptorStatusToken beforeInvocation(Object object) {
    		Assert.notNull(object, "Object was null");
    		final boolean debug = logger.isDebugEnabled();

    		if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
    			throw new IllegalArgumentException(
    					"Security invocation attempted for object "
    							+ object.getClass().getName()
    							+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
    							+ getSecureObjectClass());
    		}

        // 获取配置信息中对访问的url匹配的权限
        // 这里会返回 hasRole('ROLE_ADMIN') ,也就是之前配置的admin角色
    		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
    				.getAttributes(object);

    		if (attributes == null || attributes.isEmpty()) {
    			if (rejectPublicInvocations) {
    				throw new IllegalArgumentException(
    						"Secure object invocation "
    								+ object
    								+ " was denied as public invocations are not allowed via this interceptor. "
    								+ "This indicates a configuration error because the "
    								+ "rejectPublicInvocations property is set to 'true'");
    			}

    			if (debug) {
    				logger.debug("Public object - authentication not attempted");
    			}

    			publishEvent(new PublicInvocationEvent(object));

    			return null; // no further work post-invocation
    		}

    		if (debug) {
    			logger.debug("Secure object: " + object + "; Attributes: " + attributes);
    		}

    		if (SecurityContextHolder.getContext().getAuthentication() == null) {
    			credentialsNotFound(messages.getMessage(
    					"AbstractSecurityInterceptor.authenticationNotFound",
    					"An Authentication object was not found in the SecurityContext"),
    					object, attributes);
    		}

        // 获取身份信息
    		Authentication authenticated = authenticateIfRequired();

    		// Attempt authorization 尝试授权,也就是投票
    		try {
          //  accessDecisionManager 的实现是 AffirmativeBased,里面只有一个 WebExpressionVoter
          // 如果没有通过就会抛出异常
    			this.accessDecisionManager.decide(authenticated, object, attributes);
    		}
    		catch (AccessDeniedException accessDeniedException) {
    			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
    					accessDeniedException));
          // 在这里异常会被 org.springframework.security.web.access.ExceptionTranslationFilter#doFilter 捕获到
    			throw accessDeniedException;
    		}

    		if (debug) {
    			logger.debug("Authorization successful");
    		}

    		if (publishAuthorizationSuccess) {
    			publishEvent(new AuthorizedEvent(object, attributes, authenticated));
    		}

    		// Attempt to run as a different user
    		Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
    				attributes);

    		if (runAs == null) {
    			if (debug) {
    				logger.debug("RunAsManager did not change Authentication object");
    			}

    			// no further work post-invocation
    			return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
    					attributes, object);
    		}
    		else {
    			if (debug) {
    				logger.debug("Switching to RunAs Authentication: " + runAs);
    			}

    			SecurityContext origCtx = SecurityContextHolder.getContext();
    			SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
    			SecurityContextHolder.getContext().setAuthentication(runAs);

    			// need to revert to token.Authenticated post-invocation
    			return new InterceptorStatusToken(origCtx, true, attributes, object);
    		}
    	}    
}


// 默认投票策略
public class AffirmativeBased extends AbstractAccessDecisionManager {

	public AffirmativeBased(List<AccessDecisionVoter<? extends Object>> decisionVoters) {
		super(decisionVoters);
	}

	public void decide(Authentication authentication, Object object,
			Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
		int deny = 0;

		for (AccessDecisionVoter voter : getDecisionVoters()) {
      // WebExpressionVoter 去投票
			int result = voter.vote(authentication, object, configAttributes);

			if (logger.isDebugEnabled()) {
				logger.debug("Voter: " + voter + ", returned: " + result);
			}

			switch (result) {
        // 如果 是允许访问,则立即返回,也就是只要有一个同意则放行
  			case AccessDecisionVoter.ACCESS_GRANTED:
  				return;
        // 如果返回的是拒绝则计数
  			case AccessDecisionVoter.ACCESS_DENIED:
  				deny++;

  				break;

  			default:
  				break;
  			}
		}
    // 如果没有允许访问,则判定是否是被拒绝了。
    // 被拒绝则异常给调用者
		if (deny > 0) {
			throw new AccessDeniedException(messages.getMessage(
					"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
		}

		// To get this far, every AccessDecisionVoter abstained
		checkAllowIfAllAbstainDecisions();
	}
}


public class ExceptionTranslationFilter extends GenericFilterBean {

  	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
  			throws IOException, ServletException {
  		HttpServletRequest request = (HttpServletRequest) req;
  		HttpServletResponse response = (HttpServletResponse) res;

  		try {
  			chain.doFilter(request, response);

  			logger.debug("Chain processed normally");
  		}
  		catch (IOException ex) {
  			throw ex;
  		}
  		catch (Exception ex) {
  			// Try to extract a SpringSecurityException from the stacktrace
  			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
        // 获取到异常链中,第一次抛出的是否是身份异常信息
  			RuntimeException ase = (AuthenticationException) throwableAnalyzer
  					.getFirstThrowableOfType(AuthenticationException.class, causeChain);

  			if (ase == null) {
          // 这一次流程是未登录访问
          // 所以是被拒绝访问的异常
  				ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
  						AccessDeniedException.class, causeChain);
  			}

  			if (ase != null) {
          // 检测响应是否已经提交
          // 提交过得响应已经写了 http状态码和响应头
  				if (response.isCommitted()) {
  					throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
  				}
          // 处理安全异常
  				handleSpringSecurityException(request, response, chain, ase);
  			}
  			else {
  				// Rethrow ServletExceptions and RuntimeExceptions as-is
  				if (ex instanceof ServletException) {
  					throw (ServletException) ex;
  				}
  				else if (ex instanceof RuntimeException) {
  					throw (RuntimeException) ex;
  				}

  				// Wrap other Exceptions. This shouldn't actually happen
  				// as we've already covered all the possibilities for doFilter
  				throw new RuntimeException(ex);
  			}
  		}
  	}

    private void handleSpringSecurityException(HttpServletRequest request,
  			HttpServletResponse response, FilterChain chain, RuntimeException exception)
  			throws IOException, ServletException {
  		if (exception instanceof AuthenticationException) {
  			logger.debug(
  					"Authentication exception occurred; redirecting to authentication entry point",
  					exception);

  			sendStartAuthentication(request, response, chain,
  					(AuthenticationException) exception);
  		}
  		else if (exception instanceof AccessDeniedException) {

  			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
  				logger.debug(
  						"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
  						exception);
          // 如果是 Anonymous 身份信息,又是拒绝访问异常
          // 则发送,并开始授权
  				sendStartAuthentication(
  						request,
  						response,
  						chain,
  						new InsufficientAuthenticationException(
  							messages.getMessage(
  								"ExceptionTranslationFilter.insufficientAuthentication",
  								"Full authentication is required to access this resource")));
  			}
  			else {
  				logger.debug(
  						"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
  						exception);

  				accessDeniedHandler.handle(request, response,
  						(AccessDeniedException) exception);
  			}
  		}
  	}

    protected void sendStartAuthentication(HttpServletRequest request,
      HttpServletResponse response, FilterChain chain,
      AuthenticationException reason) throws ServletException, IOException {
      SecurityContextHolder.getContext().setAuthentication(null);
      requestCache.saveRequest(request, response);
      logger.debug("Calling Authentication entry point.");
      // org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#commence
      // 该类是我们之前配置的  loginFormUrl = /authentication/require 的处理器端点
      // 该端点会获取到 之前配置的地址然后使用
      // redirectStrategy.sendRedirect(request, response, redirectUrl); 进行跳转
      // 该访问之后就完成了拒绝并引导授权的功能
      authenticationEntryPoint.commence(request, response, reason);
  }
}

登录后访问

该源码就不记录了。篇幅太多了;

权限表达式

看源码得知,最后都会转成一个表达式,然后进行投票评估;
那么有哪些表达式呢?

这些表达式的由来,由代码中的配置而来。

.antMatchers().xxx  每个函数都包装了一个表达式生成。

跟着源码得到 返回的是一个  ExpressionUrlAuthorizationConfigurer.AuthorizedUrl 对象

联合使用是通过access方法,自己写表达式

.antMatchers("xx").access("hasRole('ROLE_USER') and hasRole('ROLE_SUPER')")

那么能自定义表达式,并且使用自己的代码逻辑来判定吗?是可以的,下一节讲解;

分离配置

如下配置,一部分是安全模块的配置,一部分是使用安全模块的应用自己的业务配置;

那么怎么能把这种业务配置分离出去呢?

.antMatchers(
         SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
         SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
         "/user/regist", // 注册请求,后面会介绍怎么把这个只有使用方知道放行的配置剥离处理
         // org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
         // BasicErrorController 类提供的默认错误信息处理服务
         "/error",
         "/connect/*",
         "/auth/*",
         "/signin"
 )
 .permitAll()
 // 该路径,只允许有 ADMIN 角色的人访问
 .antMatchers(HttpMethod.GET, "/user/*").hasRole("ADMIN")

思路:

接口定义

package cn.mrcode.imooc.springsecurity.securitycore.authorize;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;

/**
 * 权限自定义配置管理
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/12 21:09
 */
public interface AuthorizeConfigManager {
    void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}
package cn.mrcode.imooc.springsecurity.securitycore.authorize;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;

/**
 * 自定义权限控制接口
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/12 21:09
 */
public interface AuthorizeConfigProvider {
    /**
     * @param config
     * @see HttpSecurity#authorizeRequests()
     */
    void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config);
}

核心实现: 通用配置的抽取,只是把app和browser中用到的配置都抽到公用的里面了

package cn.mrcode.imooc.springsecurity.securitycore.authorize;

import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityConstants;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;

/**
 * app和browser通用静态权限配置
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/12 21:12
 */
@Component
public class CommonAuthorizeConfigProvider implements AuthorizeConfigProvider {
    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
        config.antMatchers(
                SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,
                SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPEN_ID,
                SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
                securityProperties.getBrowser().getLoginPage(),
                securityProperties.getBrowser().getSignUpUrl(),
                securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".json",
                securityProperties.getBrowser().getSession().getSessionInvalidUrl() + ".html"
        ).permitAll();
        // 退出成功处理,没有默认值,所以需要判定下
        String signOutUrl = securityProperties.getBrowser().getSignOutUrl();
        if (signOutUrl != null) {
            config.antMatchers(signOutUrl).permitAll();
        }
    }
}
package cn.mrcode.imooc.springsecurity.securitycore.authorize;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/12 21:21
 */
@Component
public class DefaultAuthorizeConfigManager implements AuthorizeConfigManager {
    @Autowired
    private Set<AuthorizeConfigProvider> providers;

    @Override
    public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
        for (AuthorizeConfigProvider provider : providers) {
            provider.config(config);
        }
        // 除了上面配置的,其他的都需要登录后才能访问
        config.anyRequest().authenticated();
    }
}

浏览器中的安全配置:

// 有三个configure的方法,这里使用http参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
    applyPasswordAuthenticationConfig(http);
    SessionProperties session = securityProperties.getBrowser().getSession();
    http
            .apply(validateCodeSecurityConfig)
            .and()
            .apply(smsCodeAuthenticationSecurityConfigs)
            .and()
            .apply(imoocSocialSecurityConfig)
            .and()
            .rememberMe()
            .tokenRepository(persistentTokenRepository)
            .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
            .userDetailsService(userDetailsService)
            .and()
            .sessionManagement()
            .invalidSessionStrategy(invalidSessionStrategy)
            .maximumSessions(session.getMaximumSessions())
            .maxSessionsPreventsLogin(session.isMaxSessionsPreventsLogin())
            .expiredSessionStrategy(sessionInformationExpiredStrategy)
            .and()
            .and()
            .logout()
            .logoutSuccessHandler(logoutSuccessHandler)
            .deleteCookies("JSESSIONID")
            .and()
            .csrf()
            .disable();
    // 注入进来,然后把调用下配置对象即可
    // 可以看到上面的配置都没有了http.authorizeRequests()的配置
    // 全部由具体的去实现配置了
    // app项目中的安全配置改动其实和这里一样
    authorizeConfigManager.config(http.authorizeRequests());
}

demo项目的安全配置

package com.example.demo.security;

import cn.mrcode.imooc.springsecurity.securitycore.authorize.AuthorizeConfigProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/12 21:25
 */
@Component
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
    @Override
    public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
        config.antMatchers(
                "/user/regist", // 注册请求
                // org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
                // BasicErrorController 类提供的默认错误信息处理服务
                "/error",
                "/connect/*",
                "/auth/*",
                "/signin",
                "/social/signUp",  // app注册跳转服务
                "/swagger-ui.html",
                "/swagger-ui.html/**",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/**"
        )
                .permitAll()
                // 这里配置了一个不存在的角色。
                // 可以访问下 看是否有效果
                .antMatchers("/user/*").hasRole("xxx")
        ;
    }
}

基于数据库Rbac数据模型控制权限

前面都是讲的怎么在权限规则基本不变的情况下,怎么写代码控制权限;

这一节要实现内管系统的场景;

这些所有的信息都必须存在数据库中。因为变动频繁,员工离职、部门调动,新增权限等;

通用RBAC数据模型

Role-Based-Access Control

通常由三直系表,两张关系表

对于资源表:存储数据的表现是 某一个url的别名是菜单或则按钮;所以url和多个菜单或则按钮绑定;

这样业务人员分配权限的时候才能看得懂

虽然是多对多,但是可以根据业务需要,进行一对多,比如一个用户只能拥有一个角色;

这个数据模型可以解决ui的显示问题和后台的程序权限控制

ui显示问题:提供一个查询该用户所有资源信息,然后由前端进行控制哪些资源显示或隐藏
url的程序控制:由security来处理

数据库中的数据如何交由security?

// 任意请求都必须走自定义的表达式
// 该表达式的含义也很简单:rbacService 在容器中的beanName名称,后面的是方法名和参数名
config.anyRequest().access("@rbacService.hasPermission(request,authentication)")

只要有了入口,那么来实现这个功能;

新建一个项目,用来写这个表达式的功能;然后demo项目引用和配置表达式

security-authorize 依赖

dependencies {
    compile 'javax.servlet:javax.servlet-api'
    // 核心的类都在该包中
    compile 'org.springframework.security:spring-security-core'
}

接口

package cn.mrcode.imooc.springsecurity.securityauthorize;

import org.springframework.security.core.Authentication;

import javax.servlet.http.HttpServletRequest;

public interface RbacService {

    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

实现

package cn.mrcode.imooc.springsecurity.securityauthorize;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Set;

@Component("rbacService")
public class RbacServiceImpl implements RbacService {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        Object principal = authentication.getPrincipal();

        boolean hasPermission = false;
        // 有可能 principal 是一个Anonymous
        // 所以只要是一个UserDetails那么就能标识是经过了我们自己的数据库查询的
        // 当前需要先配置UserDetailsServices
        if (principal instanceof UserDetails) {
            String username = ((UserDetails) principal).getUsername();
            //读取用户所拥有权限的所有URL
            Set<String> urls = new HashSet<>();
            for (String url : urls) {
                if (antPathMatcher.match(url, request.getRequestURI())) {
                    hasPermission = true;
                    break;
                }
            }

        }

        return hasPermission;
    }
}

demo项目中引用该包;并配置

package com.example.demo.security;

import cn.mrcode.imooc.springsecurity.securitycore.authorize.AuthorizeConfigProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.stereotype.Component;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/12 21:25
 */
@Component
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {
    @Override
    public void config(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config) {
        config.antMatchers(
                "/user/regist", // 注册请求
                // org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
                // BasicErrorController 类提供的默认错误信息处理服务
                "/error",
                "/connect/*",
                "/auth/*",
                "/signin",
                "/social/signUp",  // app注册跳转服务
                "/swagger-ui.html",
                "/swagger-ui.html/**",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/**"
        )
                .permitAll();
        // 使用自定义的
        config.anyRequest().access("@rbacService.hasPermission(request,authentication)");
    }
}

注意:要把该包扫描到

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@SpringBootApplication(scanBasePackages =
        {
                "com.example.demo",
                "cn.mrcode.imooc.springsecurity.securitybrowser",
                "cn.mrcode.imooc.springsecurity.securityapp",
                "cn.mrcode.imooc.springsecurity.securitycore",
                "cn.mrcode.imooc.springsecurity.securityauthorize"  // 注意添加扫描包
        })
//@SpringBootApplication
@RestController
@EnableSwagger2
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello spring security";
    }
}

ok。完成

其他问题

现在有两个问题需要解决:

  1. 在通用的AuthorizeConfigProvider里面有一个 config.anyRequest()
return requestMatchers(ANY_REQUEST); 的源码,显示只能配置一个
  1. config.anyRequest()的配置也只能在所有的配置完成之后,进行调用

第一个问题

先注释掉,通用AuthorizeConfigProvider里面的;

那么所有需要登录的配置要怎么办呢?视频中说后面总结的时候讲解;这里先留坑

第二个问题

使用order注解来解决,让我们的业务配置在最后;

第一次长见识,order还能用来控制 依赖查找的顺序

@Component
public class DefaultAuthorizeConfigManager implements AuthorizeConfigManager {
    // 由于需要有序的,所以不能再使用set了
    // 依赖查找技巧
    @Autowired
    private List<AuthorizeConfigProvider> providers;

@Component
@Order(Integer.MIN_VALUE)
public class CommonAuthorizeConfigProvider implements AuthorizeConfigProvider {

@Component
@Order(Integer.MAX_VALUE)
public class DemoAuthorizeConfigProvider implements AuthorizeConfigProvider {