1. SpringSecurity的授權(quán)流程分析
回顧之前看過的一張SpringSecurity基本原理的圖:
之前說過,SpringSecurity過濾器鏈,圖中綠色的是認(rèn)證相關(guān)的,藍色部分是異常相關(guān)的,而橙色部分是授權(quán)相關(guān),今天我們就是要理清橙色部分授權(quán)相關(guān)的流程,以及實現(xiàn)動態(tài)授權(quán)。
-
首先來看看授權(quán)邏輯的入口過濾器
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
方法,過濾器在請求進來后會執(zhí)行doFilter
方法,在這個方法里,是調(diào)用本類中的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);
分三步:
- 調(diào)用了父類的方法
super.beforeInvocation(filterInvocation)
,這個是最核心的代碼,授權(quán)核心步驟就是在這一步了。 - 這一步是每個過濾器都有的一步,授權(quán)通過執(zhí)行真正的業(yè)務(wù)
- 后續(xù)的一些處理
- 調(diào)用了父類的方法
-
-
接下來看看核心的授權(quán)邏輯:
beforeInvocation
方法,在類AbstractSecurityInterceptor
中實現(xiàn)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); }
這里的源碼刪減了一些關(guān)系不大的部分,這段代碼大體可以分為三步:
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
拿到系統(tǒng)配置的URL權(quán)限,并封裝為ConfigAttribute
對象集合,其實這里面就是我們在配置文件中配置的權(quán)限通過
authenticateIfRequired
方法拿到已認(rèn)證過的Authentication
對象,其實里面還是通過SecurityContextHolder
通過上下文拿到的。-
調(diào)用
attemptAuthorization
方法去授權(quán)代碼運行到這里后,我們拿到了系統(tǒng)配置的URL權(quán)限attributes,認(rèn)證用戶對象Authentication也拿到了,還有當(dāng)前請求相關(guān)的信息FilterInvocation ,也就是這個方法的參數(shù)object,接下來授權(quán)肯定是拿著三部分的信息去實現(xiàn)的。
我們再看看這個方法具體實現(xiàn):
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; } }
這段代碼最核心就是調(diào)用了
this.accessDecisionManager.decide(authenticated, object, attributes);
,通過accessDecisionManager
進行授權(quán),并且將前面獲取到的三部分信息傳參進去。
-
緊接著來了解下這個決策管理器
AccessDecisionManager
前面我們一步步走到了授權(quán)處理方法
attemptAuthorization
,發(fā)現(xiàn)它又是調(diào)用了accessDecisionManager
的decide
方法去真正處理授權(quán)的,我們來看看這個決策管理器的源碼: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
用以授權(quán),另外兩個supports方法主要起輔助作用,大都執(zhí)行檢查操作的。既然是一個接口,那調(diào)用的肯定是實現(xiàn)類了,我們可以接著看看他有哪些實現(xiàn)類:
AccessDecisionManager.png從圖中可以看到它有一個抽象實現(xiàn)類,然后抽象實現(xiàn)類下又有三個實現(xiàn)類,我們可以通過Debug看看默認(rèn)實現(xiàn)的是哪個
attemptAuthorization.png可以看到SpringSecurity默認(rèn)的實現(xiàn)類是
AffirmativeBased
再來看看這三種不同的授權(quán)邏輯,分別為:
-
AffirmativeBased
:默認(rèn)的實現(xiàn)類,一票通過制,只要有一票同意則通過 -
ConsensusBased
:一票反對制,只要有一票反對都不能通過 -
UnanimousBased
:少數(shù)服從多數(shù)制,以多數(shù)票為結(jié)果
這里之所以用投票來形容,是因為這個決策管理器采用了委托的形式,將請求委托給了投票器,由每個投票器去決策,這么一來,說明真正決策的并不是這三種實現(xiàn)類,而是投票器。
那就接著跟著默認(rèn)的實現(xiàn)類
AffirmativeBased
源碼看看具體的實現(xiàn)。 -
-
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(); } }
前面說了實現(xiàn)類是委托給了投票器進行決策的,從源碼中也可以看到,這里是通過輪詢所有配置的
AccessDecisionVoter
,根據(jù)投票器的結(jié)果進行權(quán)限授予。這里的
getDecisionVoters
方法是在父類AbstractAccessDecisionManager
中實現(xiàn)的,源碼中就是在構(gòu)造器AbstractAccessDecisionManager
中傳入Voter的列表,而在類AffirmativeBased
的構(gòu)造器中調(diào)用了父類的構(gòu)造器super(decisionVoters);
,也就是說最終由多少個AccessDecisionVoter
是AffirmativeBased
的構(gòu)造器中注入的,是一個List。我們再debug下看看,這個List有多少個投票器
getDecisionVoters.png可以看到,默認(rèn)只有一個投票器
WebExpressionVoter
,這個投票器會根據(jù)我們在配置文件中的配置進行邏輯處理得出投票結(jié)果。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; } // 循環(huán)判斷,只要有一個權(quán)限符合就返回 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
方法 -
總結(jié)流程
通過上面的分析,我們大體能了解到整個授權(quán)的流程是這樣的:(網(wǎng)上找的圖)
accessDecision-flow.png
2. 動態(tài)權(quán)限的實現(xiàn)
通過上面的授權(quán)流程分析,咱們大致清楚了SpringSecurity是怎么授權(quán)的,那么我們要實現(xiàn)動態(tài)授權(quán)應(yīng)該怎么做?其實就是實現(xiàn)自定義上圖中的兩個類:一個是SecurityMetadataSource
類用來獲取當(dāng)前請求所需要的權(quán)限;另一個是AccessDecisionManager
類來實現(xiàn)授權(quán)決策
宗旨就是需要三個數(shù)據(jù):請求所需的權(quán)限,能獲取到該請求的Object,以及已認(rèn)證對象所擁有的權(quán)限。(其實就是投票器執(zhí)行方法decide
的三個參數(shù))
下面就以實現(xiàn)SecurityMetadataSource
類和AccessDecisionManager
類的方式來實現(xiàn)動態(tài)授權(quán)
-
數(shù)據(jù)庫結(jié)構(gòu)
建立user用戶表,role角色表,resource資源表以及user_role表,resource_role表
預(yù)先插入一些數(shù)據(jù),如下圖
用戶表:(密碼都是經(jīng)過加密的,分別是123,admin,user)
table_user.png角色表:
table_role.png資源表:
table_resource.png用戶-角色關(guān)系表:
table_user_role.png資源角色關(guān)系表:
table_resource_role.png -
創(chuàng)建實體類: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; /** * 獲取用戶的權(quán)限信息,封裝為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; }
-
創(chuàng)建Mapper類和xml
@Mapper public interface UserMapper { /** * 根據(jù)用戶名獲取用戶信息 * @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>
-
創(chuàng)建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("用戶不存在!"); } // 將用戶權(quán)限填充進去 user.setRoleList(userMapper.getRolesByUserId(user.getId())); return user; } }
-
自定義一個
FilterInvocationSecurityMetadataSource
實現(xiàn)類,實現(xiàn)getAttributes方法public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private ResourceMapper resourceMapper; // Ant風(fēng)格匹配器 private final AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * 獲取當(dāng)前請求所需要的權(quán)限 * @param object * @return * @throws IllegalArgumentException */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { FilterInvocation request = (FilterInvocation) object; String url = request.getRequestUrl(); // 獲取所有的資源與對應(yīng)角色信息 List<Resource> resources = resourceMapper.getAllResourceWithRole(); // 遍歷所有的資源,查找與當(dāng)前請求匹配的url for (Resource resource : resources) { // 匹配上 if (antPathMatcher.match(resource.getUrl(), url)) { // 獲取url對應(yīng)的角色信息 List<Role> roles = resource.getRoleList(); String[] roleStr = new String[roles.size()]; // 遍歷roles集合,將每一個角色信息轉(zhuǎn)為字符串形式 for (int i = 0; i < roleStr.length; i++) { roleStr[i] = roles.get(i).getName(); } // 返回所有的角色信息 return SecurityConfig.createList(roleStr); } } // 如果沒有匹配上的,就返回一個自定義的作為個標(biāo)記,只要是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
實現(xiàn)類,重寫decide方法public class CustomAccessDecisionManager implements AccessDecisionManager { /** * 權(quán)限決策 * @param authentication 已認(rèn)證用戶對象 * @param object 包含請求相關(guān)信息的FilterInvocation * @param configAttributes 當(dāng)前請求所需要的角色信息 * @throws AccessDeniedException * @throws InsufficientAuthenticationException */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { // 獲取認(rèn)證的用戶所具有的角色權(quán)限 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 循環(huán)遍歷當(dāng)前請求所需的角色信息,只要有一個滿足就可以 for (ConfigAttribute configAttribute : configAttributes) { // 該請求在數(shù)據(jù)庫中不具備角色 if ("ROLE_null".equals(configAttribute.getAttribute()) && authentication instanceof UsernamePasswordAuthenticationToken) { return; } // 輪詢判斷用戶的角色權(quán)限是否符合當(dāng)前資源請求的所需要的權(quán)限 for (GrantedAuthority authority : authorities) { System.out.println("authority = " + authority.getAuthority()); if (authority.getAuthority().equals(configAttribute.getAttribute())){ return; } } } throw new AccessDeniedException("權(quán)限不足,無法訪問!"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
-
創(chuàng)建配置類
@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", "沒有權(quán)限!"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getWriter().write(new ObjectMapper().writeValueAsString(map)); } }
-
創(chuàng)建測試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用戶來測試,在設(shè)計的數(shù)據(jù)庫表中,root用戶只有訪問/root/**的權(quán)限,其他的沒有權(quán)限
首先先訪問
http://localhost:8080/login
,輸入賬號密碼登錄-
首先訪問
/root/hello
-root-hello.png -
然后再訪問下
/admin/hello
,因為/admin/hello
這個資源路徑所需的角色信息是ROLE_admin,所以root用戶是沒有權(quán)限訪問的-admin-hello.png -
接著咱們來試試動態(tài)權(quán)限,在數(shù)據(jù)庫
resource_role
中插入一行,賦予root用戶訪問/admin/**
的權(quán)限INSERT INTO resource_role VALUES(NULL, 2, 1)
資源
/admin/**
的id是2,ROLE_root角色的id是1。現(xiàn)在root用戶就擁有了訪問
/admin/**
的權(quán)限了,我們可以再次訪問驗證:-root-admin-hello.png至此,我們就可以動態(tài)的對權(quán)限做出控制,賦予資源路徑的訪問角色,從而決定用戶的訪問權(quán)限
-
源碼地址
源碼我已經(jīng)放到了gitee上了,地址是: Lucas-張 / SpringSecurity