SpringSecurity授权流程分析+动态授权实现
1. SpringSecurity的授权流程分析
回顾之前看过的一张SpringSecurity基本原理的图:
之前说过,SpringSecurity过滤器链,图中绿色的是认证相关的,蓝色部分是异常相关的,而橙色部分是授权相关,今天我们就是要理清橙色部分授权相关的流程,以及实现动态授权。
-
首先来看看授权逻辑的入口过滤器
FilterSecurityInterceptor
源码public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { invoke(new FilterInvocation(request, response, chain)); }
FilterSecurityInterceptor
的主要方法是doFilter
方法,过滤器在请求进来后会执行doFilter
方法,在这个方法里,是调用本类中的invoke
方法,所以invoke
方法才是主要逻辑的地方public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException { if (isApplied(filterInvocation) && this.observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling, so don't re-do security checking filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); return; } // first time this request being called, so perform security checking if (filterInvocation.getRequest() != null && this.observeOncePerRequest) { filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(filterInvocation); try { filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); }
-
这里最核心的就是最后这几句了:
InterceptorStatusToken token = super.beforeInvocation(filterInvocation); try { filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null);
分三步:
- 调用了父类的方法
super.beforeInvocation(filterInvocation)
,这个是最核心的代码,授权核心步骤就是在这一步了。 - 这一步是每个过滤器都有的一步,授权通过执行真正的业务
- 后续的一些处理
- 调用了父类的方法
-
-
接下来看看核心的授权逻辑:
beforeInvocation
方法,在类AbstractSecurityInterceptor
中实现protected InterceptorStatusToken beforeInvocation(Object object) { Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object); Authentication authenticated = authenticateIfRequired(); // Attempt authorization attemptAuthorization(object, attributes, authenticated); if (this.publishAuthorizationSuccess) { publishEvent(new AuthorizedEvent(object, attributes, authenticated)); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); }
这里的源码删减了一些关系不大的部分,这段代码大体可以分为三步:
-
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
拿到系统配置的URL权限,并封装为ConfigAttribute
对象集合,其实这里面就是我们在配置文件中配置的权限 -
通过
authenticateIfRequired
方法拿到已认证过的Authentication
对象,其实里面还是通过SecurityContextHolder
通过上下文拿到的。 -
调用
attemptAuthorization
方法去授权代码运行到这里后,我们拿到了系统配置的URL权限attributes,认证用户对象Authentication也拿到了,还有当前请求相关的信息FilterInvocation ,也就是这个方法的参数object,接下来授权肯定是拿着三部分的信息去实现的。
我们再看看这个方法具体实现:
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) { try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException ex) { if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,attributes, this.accessDecisionManager)); } else if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes)); } publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex)); throw ex; } }
这段代码最核心就是调用了
this.accessDecisionManager.decide(authenticated, object, attributes);
,通过accessDecisionManager
进行授权,并且将前面获取到的三部分信息传参进去。
-
-
紧接着来了解下这个决策管理器
AccessDecisionManager
前面我们一步步走到了授权处理方法
attemptAuthorization
,发现它又是调用了accessDecisionManager
的decide
方法去真正处理授权的,我们来看看这个决策管理器的源码:public interface AccessDecisionManager { void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); }
从源码可知这个
AccessDecisionManager
是一个接口,声明了三个方法,核心方法是decide
用以授权,另外两个supports方法主要起辅助作用,大都执行检查操作的。既然是一个接口,那调用的肯定是实现类了,我们可以接着看看他有哪些实现类:
从图中可以看到它有一个抽象实现类,然后抽象实现类下又有三个实现类,我们可以通过Debug看看默认实现的是哪个
可以看到SpringSecurity默认的实现类是
AffirmativeBased
再来看看这三种不同的授权逻辑,分别为:
AffirmativeBased
:默认的实现类,一票通过制,只要有一票同意则通过ConsensusBased
:一票反对制,只要有一票反对都不能通过UnanimousBased
:少数服从多数制,以多数票为结果
这里之所以用投票来形容,是因为这个决策管理器采用了委托的形式,将请求委托给了投票器,由每个投票器去决策,这么一来,说明真正决策的并不是这三种实现类,而是投票器。
那就接着跟着默认的实现类
AffirmativeBased
源码看看具体的实现。 -
AffirmativeBased
源码public class AffirmativeBased extends AbstractAccessDecisionManager { public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) { super(decisionVoters); } @Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { throw new AccessDeniedException( this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); } }
前面说了实现类是委托给了投票器进行决策的,从源码中也可以看到,这里是通过轮询所有配置的
AccessDecisionVoter
,根据投票器的结果进行权限授予。这里的
getDecisionVoters
方法是在父类AbstractAccessDecisionManager
中实现的,源码中就是在构造器AbstractAccessDecisionManager
中传入Voter的列表,而在类AffirmativeBased
的构造器中调用了父类的构造器super(decisionVoters);
,也就是说最终由多少个AccessDecisionVoter
是AffirmativeBased
的构造器中注入的,是一个List。我们再debug下看看,这个List有多少个投票器
可以看到,默认只有一个投票器
WebExpressionVoter
,这个投票器会根据我们在配置文件中的配置进行逻辑处理得出投票结果。public class WebExpressionVoter implements AccessDecisionVoter<FilterInvocation> { @Override public int vote(Authentication authentication, FilterInvocation filterInvocation, Collection<ConfigAttribute> attributes) { Assert.notNull(authentication, "authentication must not be null"); Assert.notNull(filterInvocation, "filterInvocation must not be null"); Assert.notNull(attributes, "attributes must not be null"); WebExpressionConfigAttribute webExpressionConfigAttribute = findConfigAttribute(attributes); if (webExpressionConfigAttribute == null) { this.logger .trace("Abstained since did not find a config attribute of instance WebExpressionConfigAttribute"); return ACCESS_ABSTAIN; } EvaluationContext ctx = webExpressionConfigAttribute.postProcess( this.expressionHandler.createEvaluationContext(authentication, filterInvocation), filterInvocation); boolean granted = ExpressionUtils.evaluateAsBoolean(webExpressionConfigAttribute.getAuthorizeExpression(), ctx); if (granted) { return ACCESS_GRANTED; } this.logger.trace("Voted to deny authorization"); return ACCESS_DENIED; } // 循环判断,只要有一个权限符合就返回 private WebExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) { for (ConfigAttribute attribute : attributes) { if (attribute instanceof WebExpressionConfigAttribute) { return (WebExpressionConfigAttribute) attribute; } } return null; } }
-
最后来看看返回的过程
从投票器
WebExpressionVoter
返回到AffirmativeBased
的decide
方法@Override @SuppressWarnings({ "rawtypes", "unchecked" }) public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException { int deny = 0; for (AccessDecisionVoter voter : getDecisionVoters()) { int result = voter.vote(authentication, object, configAttributes); switch (result) { case AccessDecisionVoter.ACCESS_GRANTED: return; case AccessDecisionVoter.ACCESS_DENIED: deny++; break; default: break; } } if (deny > 0) { throw new AccessDeniedException( this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied")); } // To get this far, every AccessDecisionVoter abstained checkAllowIfAllAbstainDecisions(); }
如果投票通过,直接return,没有返回值,回到了
AbstractSecurityInterceptor
的attemptAuthorization
方法private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,Authentication authenticated) { try { this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException ex) { if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Failed to authorize %s with attributes %s using %s", object,attributes, this.accessDecisionManager)); } else if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Failed to authorize %s with attributes %s", object, attributes)); } publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, ex)); throw ex; } }
再回到了
beforeInvocation
方法,最后回到了最开始的过滤器FilterSecurityInterceptor
的invoke
方法 -
总结流程
通过上面的分析,我们大体能了解到整个授权的流程是这样的:(网上找的图)
2. 动态权限的实现
通过上面的授权流程分析,咱们大致清楚了SpringSecurity是怎么授权的,那么我们要实现动态授权应该怎么做?其实就是实现自定义上图中的两个类:一个是SecurityMetadataSource
类用来获取当前请求所需要的权限;另一个是AccessDecisionManager
类来实现授权决策
宗旨就是需要三个数据:请求所需的权限,能获取到该请求的Object,以及已认证对象所拥有的权限。(其实就是投票器执行方法decide
的三个参数)
下面就以实现SecurityMetadataSource
类和AccessDecisionManager
类的方式来实现动态授权
-
数据库结构
建立user用户表,role角色表,resource资源表以及user_role表,resource_role表
预先插入一些数据,如下图
用户表:(密码都是经过加密的,分别是123,admin,user)
角色表:
资源表:
用户-角色关系表:
资源角色关系表:
-
创建实体类:User,Role,Resource
@Data public class User implements UserDetails { private static final long serialVersionUID = -3185138705702678193L; private Integer id; private String username; private String password; private boolean enabled; private boolean locked; private List<Role> roleList; /** * 获取用户的权限信息,封装为GrantedAuthority * @return */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (Role role : roleList) { authorities.add(new SimpleGrantedAuthority(role.getName())); } return authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !locked; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return this.enabled; } }
@Data public class Role { // 角色ID private Integer id; // 角色英文名 private String name; // 角色中文名 private String nameZh; }
@Data public class Resource { // 资源ID private Integer id; // 资源路径 private String url; // 角色列表 private List<Role> roleList; }
-
创建Mapper类和xml
@Mapper public interface UserMapper { /** * 根据用户名获取用户信息 * @param username * @return */ User getUserByUsername(@Param("username") String username); /** * 获取指定ID的用户的所有角色信息 * @param id * @return */ List<Role> getRolesByUserId(@Param("userId") Integer id); /** * 插入一个用户 * @param user * @return */ int insertOneUser(@Param("user") User user); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zzy.accessdecision.mapper.UserMapper"> <select id="getUserByUsername" resultType="user"> select * from user where username = #{username}; </select> <select id="getRolesByUserId" resultType="role"> select * from role where id in (select rid from user_role where uid = #{userId}); </select> <insert id="insertOneUser"> insert into user values(#{user.id}, #{user.username}, #{user.password}, #{user.enabled}, #{user.locked}); </insert> </mapper>
@Mapper public interface ResourceMapper { List<Resource> getAllResourceWithRole(); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zzy.accessdecision.mapper.ResourceMapper"> <resultMap id="resources_map" type="resource"> <id property="id" column="id"/> <result property="url" column="url"/> <collection property="roleList" ofType="role"> <id property="id" column="rid" /> <result property="name" column="name"/> <result property="nameZh" column="nameZh"/> </collection> </resultMap> <select id="getAllResourceWithRole" resultMap="resources_map"> select resource.*, role.id as rid, role.name, role.nameZh from resource left join resource_role on resource.id = resource_role.resource_id left join role on resource_role.role_id = role.id </select> </mapper>
-
创建UserService
@Service public class UserService implements UserDetailsService { @Autowired private UserMapper userMapper; /** * 通过用户名获取用户信息 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.getUserByUsername(username); if (user == null) { throw new UsernameNotFoundException("用户不存在!"); } // 将用户权限填充进去 user.setRoleList(userMapper.getRolesByUserId(user.getId())); return user; } }
-
自定义一个
FilterInvocationSecurityMetadataSource
实现类,实现getAttributes方法public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private ResourceMapper resourceMapper; // Ant风格匹配器 private final AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * 获取当前请求所需要的权限 * @param object * @return * @throws IllegalArgumentException */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { FilterInvocation request = (FilterInvocation) object; String url = request.getRequestUrl(); // 获取所有的资源与对应角色信息 List<Resource> resources = resourceMapper.getAllResourceWithRole(); // 遍历所有的资源,查找与当前请求匹配的url for (Resource resource : resources) { // 匹配上 if (antPathMatcher.match(resource.getUrl(), url)) { // 获取url对应的角色信息 List<Role> roles = resource.getRoleList(); String[] roleStr = new String[roles.size()]; // 遍历roles集合,将每一个角色信息转为字符串形式 for (int i = 0; i < roleStr.length; i++) { roleStr[i] = roles.get(i).getName(); } // 返回所有的角色信息 return SecurityConfig.createList(roleStr); } } // 如果没有匹配上的,就返回一个自定义的作为个标记,只要是ROLE_null则说明不匹配 return SecurityConfig.createList("ROLE_null"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } /** * 校验类是否支持 * @param clazz * @return */ @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }
-
自定义
AccessDecisionManager
实现类,重写decide方法public class CustomAccessDecisionManager implements AccessDecisionManager { /** * 权限决策 * @param authentication 已认证用户对象 * @param object 包含请求相关信息的FilterInvocation * @param configAttributes 当前请求所需要的角色信息 * @throws AccessDeniedException * @throws InsufficientAuthenticationException */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { // 获取认证的用户所具有的角色权限 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 循环遍历当前请求所需的角色信息,只要有一个满足就可以 for (ConfigAttribute configAttribute : configAttributes) { // 该请求在数据库中不具备角色 if ("ROLE_null".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken) { return; } // 轮询判断用户的角色权限是否符合当前资源请求的所需要的权限 for (GrantedAuthority authority : authorities) { System.out.println("authority = " + authority.getAuthority()); if (authority.getAuthority().equals(configAttribute.getAttribute())){ return; } } } throw new AccessDeniedException("权限不足,无法访问!"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
-
创建配置类
@Configuration public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource() { return new CustomFilterInvocationSecurityMetadataSource(); } @Bean public CustomAccessDecisionManager customAccessDecisionManager() { return new CustomAccessDecisionManager(); } @Override public void configure(AuthenticationManagerBuilder builder) throws Exception { builder.userDetailsService(userService).passwordEncoder(passwordEncoder()); } @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/register", "/index"); } @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource()); object.setAccessDecisionManager(customAccessDecisionManager()); return object; } }) .and() .formLogin() .loginProcessingUrl("/login").successForwardUrl("/index") .permitAll() .and() .csrf() .disable(); http.exceptionHandling().accessDeniedHandler(new SimpleAccessDeniedHandler()); } }
-
自定义异常处理
public class SimpleAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { Map<String, Object> map = new HashMap<>(); map.put("status", HttpServletResponse.SC_FORBIDDEN); map.put("msg", "没有权限!"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getWriter().write(new ObjectMapper().writeValueAsString(map)); } }
-
创建测试Controller
@RestController public class TestController { @GetMapping("/hello") public String hello() { return "hello!"; } @RequestMapping("/index") public String index() { return "index!"; } @GetMapping("/root/hello") public String root() { return "hello root!"; } @GetMapping("/admin/hello") public String admin() { return "hello admin!"; } @GetMapping("/user/hello") public String user() { return "hello user!"; } }
-
开始测试!
以root用户来测试,在设计的数据库表中,root用户只有访问/root/**的权限,其他的没有权限
-
首先先访问
http://localhost:8080/login
,输入账号密码登录 -
首先访问
/root/hello
-
然后再访问下
/admin/hello
,因为/admin/hello
这个资源路径所需的角色信息是ROLE_admin,所以root用户是没有权限访问的 -
接着咱们来试试动态权限,在数据库
resource_role
中插入一行,赋予root用户访问/admin/**
的权限INSERT INTO resource_role VALUES(NULL, 2, 1)
资源
/admin/**
的id是2,ROLE_root角色的id是1。现在root用户就拥有了访问
/admin/**
的权限了,我们可以再次访问验证:至此,我们就可以动态的对权限做出控制,赋予资源路径的访问角色,从而决定用户的访问权限
-
-
源码地址
源码我已经放到了gitee上了,地址是: Lucas-张 / SpringSecurity