如何动态处理角色和资源的关系

1.创建User和UserService

首先先创建User类,即我们的用户类,该类实现了UserDetails接口

public class User implements UserDetails {
    private Long id;
    private String name;
    private String phone;
    private String telephone;
    private String address;
    private boolean enabled;
    private String username;
    private String password;
    private String remark;
    private List<Role> roles;
    private String userface;
    //getter/setter省略
}

1.UserDetails 接口默认有几个方法需要实现,这几个方法中,除了 isEnabled 返回了正常的 enabled 之外,其他的方法我都统一返回 true,因为我这里的业务逻辑并不涉及到账户的锁定、密码的过期等等,只有账户是否被禁用,因此只处理了 isEnabled 方法,这一块小伙伴可以根据自己的实际情况来调整。
2.另外,UserDetails 中还有一个方法叫做 getAuthorities,该方法用来获取当前用户所具有的角色,但是小伙伴也看到了,我的 User中有一个 roles 属性用来描述当前用户的角色,因此我的 getAuthorities 方法的实现如下:

public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> authorities = new ArrayList<>();
    for (Role role : roles) {
        authorities.add(new SimpleGrantedAuthority(role.getName()));
    }
    return authorities;
}

1.即直接从 roles 中获取当前用户所具有的角色,构造 SimpleGrantedAuthority 然后返回即可。
2.创建好 User 之后,接下来我们需要创建 UserService,用来执行登录等操作,UserService需要实现 UserDetailsService 接口,如下:

@Service
@Transactional
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user= userMapper.loadUserByUsername(s);
        if (user== null) {
            throw new UsernameNotFoundException("用户名不对");
        }
        return user;
    }
}

1.这里最主要是实现了 UserDetailsService 接口中的 loadUserByUsername 方法,在执行登录的过程中,这个方法将根据用户名去查找用户,如果用户不存在,则抛出 UsernameNotFoundException 异常,否则直接将查到的 User 返回。
2.UserMapper 用来执行数据库的查询操作,这个不在本系列的介绍范围内,所有涉及到数据库的操作都将只介绍方法的作用。

2.自定义FilterInvocationSecurityMetadataSource

FilterInvocationSecurityMetadataSource 有一个默认的实现类DefaultFilterInvocationSecurityMetadataSource,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色,我们照猫画虎,自己也定义一个 FilterInvocationSecurityMetadataSource,如下:

@Component
public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuService menuService;
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        //获取请求地址
        String requestUrl = ((FilterInvocation) o).getRequestUrl();
        if ("/login_p".equals(requestUrl)) {
            return null;
        }
        List<Menu> allMenu = menuService.getAllMenu();
        for (Menu menu : allMenu) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)&&menu.getRoles().size()>0) {
                List<Role> roles = menu.getRoles();
                int size = roles.size();
                String[] values = new String[size];
                for (int i = 0; i < size; i++) {
                    values[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(values);
            }
        }
        //没有匹配上的资源,都是登录访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return FilterInvocation.class.isAssignableFrom(aClass);
    }
}

1.一开始注入了 MenuService,MenuService 的作用是用来查询数据库中 url pattern 和 role 的对应关系,查询结果是一个 List 集合,集合中是 Menu 类,Menu 类有两个核心属性,一个是 url pattern,即匹配规则(比如 /admin/** ),还有一个是 List,即这种规则的路径需要哪些角色才能访问。
2.我们可以从 getAttributes(Object o) 方法的参数 o 中提取出当前的请求 url,然后将这个请求 url 和数据库中查询出来的所有 url pattern一一对照,看符合哪一个 url pattern,然后就获取到该 url pattern 所对应的角色,当然这个角色可能有多个,所以遍历角色,最后利用 SecurityConfig.createList 方法来创建一个角色集合。
3.第二步的操作中,涉及到一个优先级问题,比如我的地址是 /employee/basic/hello ,这个地址既能被 /employee/** 匹配,也能被 /employee/basic/** 匹配,这就要求我们从数据库查询的时候对数据进行排序,将 /employee/basic/** 类型的 url pattern 放在集合的前面去比较。
4.如果 getAttributes(Object o) 方法返回 null 的话,意味着当前这个请求不需要任何角色就能访问,甚至不需要登录。但是在我的整个业务中,并不存在这样的请求,我这里的要求是,所有未匹配到的路径,都是认证(登录)后可访问,因此我在这里返回一个 ROLE_LOGIN 的角色,这种角色在我的角色数据库中并不存在,因此我将在下一步的角色比对过程中特殊处理这种角色。
5.如果地址是 /login_p,这个是登录页,不需要任何角色即可访问,直接返回 null。
6.getAttributes(Object o) 方法返回的集合最终会来到 AccessDecisionManager 类中,接下来我们再来看 AccessDecisionManager 类。

3.自定义 AccessDecisionManager

自定义 UrlAccessDecisionManager 类实现 AccessDecisionManager 接口,如下:

@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, AuthenticationException {
        Iterator<ConfigAttribute> iterator = collection.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute ca = iterator.next();
            //当前请求需要的权限
            String needRole = ca.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("未登录");
                } else
                    return;
            }
            //当前用户所具有的权限
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

1.decide 方法接收三个参数,其中第一个参数中保存了当前登录用户的角色信息,第三个参数则是 UrlFilterInvocationSecurityMetadataSource 中的 getAttributes 方法传来的,表示当前请求需要的角色(可能有多个)。
2.如果当前请求需要的权限为 ROLE_LOGIN 则表示登录即可访问,和角色没有关系,此时我需要判断 authentication 是不是 AnonymousAuthenticationToken 的一个实例,如果是,则表示当前用户没有登录,没有登录就抛一个 BadCredentialsException 异常,登录了就直接返回,则这个请求将被成功执行。
3.遍历 collection,同时查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常。
4.这里涉及到一个 all 和 any 的问题:假设当前用户具备角色 A、角色 B,当前请求需要角色 B、角色 C,那么是要当前用户要包含所有请求角色才算授权成功还是只要包含一个就算授权成功?我这里采用了第二种方案,即只要包含一个即可。小伙伴可根据自己的实际情况调整 decide 方法中的逻辑。

4.自定义AccessDeniedHandler

通过自定义 AccessDeniedHandler 我们可以自定义 403 响应的内容,如下:

@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        resp.setCharacterEncoding("UTF-8");
        PrintWriter out = resp.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
        out.flush();
        out.close();
    }
}

5.配置 WebSecurityConfig

最后在 webSecurityConfig 中完成简单的配置即可,如下:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;
    @Autowired
    UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
    @Autowired
    UrlAccessDecisionManager urlAccessDecisionManager;
    @Autowired
    AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/index.html", "/static/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                        o.setSecurityMetadataSource(urlFilterInvocationSecurityMetadataSource);
                        o.setAccessDecisionManager(urlAccessDecisionManager);
                        return o;
                    }
                }).and().formLogin().loginPage("/login_p").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                StringBuffer sb = new StringBuffer();
                sb.append("{\"status\":\"error\",\"msg\":\"");
                if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                    sb.append("用户名或密码输入错误,登录失败!");
                } else if (e instanceof DisabledException) {
                    sb.append("账户被禁用,登录失败,请联系管理员!");
                } else {
                    sb.append("登录失败!");
                }
                sb.append("\"}");
                out.write(sb.toString());
                out.flush();
                out.close();
            }
        }).successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                ObjectMapper objectMapper = new ObjectMapper();
                String s = "{\"status\":\"success\",\"msg\":" + objectMapper.writeValueAsString(UserUtils.getCurrentUser()) + "}";
                out.write(s);
                out.flush();
                out.close();
            }
        }).and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler);
    }
}

1.在 configure(HttpSecurity http) 方法中,通过 withObjectPostProcessor 将刚刚创建的 UrlFilterInvocationSecurityMetadataSource 和 UrlAccessDecisionManager 注入进来。到时候,请求都会经过刚才的过滤器(除了 configure(WebSecurity web)方法忽略的请求)。
2.successHandler 中配置登录成功时返回的 JSON,登录成功时返回当前用户的信息。
3.failureHandler 表示登录失败,登录失败的原因可能有多种,我们根据不同的异常输出不同的错误提示即可。

OK,这些操作都完成之后,我们可以通过 POSTMAN 或者 RESTClient 来发起一个登录请求就可以看到如下信息

{
   "status": "success",
   "msg": "登录成功"
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容