SpringSecurity授權(quán)流程分析+動態(tài)授權(quán)實現(xiàn)

1. SpringSecurity的授權(quán)流程分析

回顧之前看過的一張SpringSecurity基本原理的圖:

authentication_1.jpeg

之前說過,SpringSecurity過濾器鏈,圖中綠色的是認(rèn)證相關(guān)的,藍色部分是異常相關(guān)的,而橙色部分是授權(quán)相關(guān),今天我們就是要理清橙色部分授權(quán)相關(guān)的流程,以及實現(xiàn)動態(tài)授權(quán)。

  1. 首先來看看授權(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);
    }
    
    1. 這里最核心的就是最后這幾句了:

      InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
      try {
          filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
      }
      finally {
          super.finallyInvocation(token);
      }
      super.afterInvocation(token, null);
      

      分三步:

      1. 調(diào)用了父類的方法super.beforeInvocation(filterInvocation),這個是最核心的代碼,授權(quán)核心步驟就是在這一步了。
      2. 這一步是每個過濾器都有的一步,授權(quán)通過執(zhí)行真正的業(yè)務(wù)
      3. 后續(xù)的一些處理
  2. 接下來看看核心的授權(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)系不大的部分,這段代碼大體可以分為三步:

    1. Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);拿到系統(tǒng)配置的URL權(quán)限,并封裝為ConfigAttribute對象集合,其實這里面就是我們在配置文件中配置的權(quán)限

    2. 通過authenticateIfRequired方法拿到已認(rèn)證過的Authentication對象,其實里面還是通過SecurityContextHolder通過上下文拿到的。

    3. 調(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),并且將前面獲取到的三部分信息傳參進去。

  3. 緊接著來了解下這個決策管理器AccessDecisionManager

    前面我們一步步走到了授權(quán)處理方法attemptAuthorization,發(fā)現(xiàn)它又是調(diào)用了accessDecisionManagerdecide方法去真正處理授權(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)。

  4. 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);,也就是說最終由多少個AccessDecisionVoterAffirmativeBased的構(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;
        }
    }
    
  5. 最后來看看返回的過程

    從投票器WebExpressionVoter返回到AffirmativeBaseddecide方法

    @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,沒有返回值,回到了AbstractSecurityInterceptorattemptAuthorization方法

    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方法,最后回到了最開始的過濾器FilterSecurityInterceptorinvoke方法

  6. 總結(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)

  1. 數(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
  2. 創(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;
    
    }
    
  3. 創(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>
    
  4. 創(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;
        }
    }
    
  5. 自定義一個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);
        }
    }
    
  6. 自定義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;
        }
    }
    
  7. 創(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());
        }
    }
    
  8. 自定義異常處理

    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));
        }
    }
    
  9. 創(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!";
        }
    }
    
  10. 開始測試!

    以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)限

  11. 源碼地址

    源碼我已經(jīng)放到了gitee上了,地址是: Lucas-張 / SpringSecurity

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容