首先我们需要创建Hr类,即我们的用户类,该类实现了UserDetails接口,该类的属性如下:

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

  1. List<GrantedAuthority> authorities = new ArrayList<>();
  2. for (Role role : roles) {
  3. authorities.add(new SimpleGrantedAuthority(role.getName()));
  4. }
  5. return authorities;
  6. }

即直接从roles中获取当前用户所具有的角色,构造SimpleGrantedAuthority然后返回即可。

创建好Hr之后,接下来我们需要创建HrService,用来执行登录等操作,HrService需要实现UserDetailsService接口,如下:

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

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

  1. @Component
  2. public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
  3. @Autowired
  4. MenuService menuService;
  5. AntPathMatcher antPathMatcher = new AntPathMatcher();
  6. @Override
  7. public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
  8. //获取请求地址
  9. if ("/login_p".equals(requestUrl)) {
  10. return null;
  11. }
  12. List<Menu> allMenu = menuService.getAllMenu();
  13. if (antPathMatcher.match(menu.getUrl(), requestUrl)&&menu.getRoles().size()>0) {
  14. List<Role> roles = menu.getRoles();
  15. int size = roles.size();
  16. String[] values = new String[size];
  17. for (int i = 0; i < size; i++) {
  18. values[i] = roles.get(i).getName();
  19. }
  20. return SecurityConfig.createList(values);
  21. }
  22. }
  23. //没有匹配上的资源,都是登录访问
  24. return SecurityConfig.createList("ROLE_LOGIN");
  25. }
  26. @Override
  27. return null;
  28. }
  29. @Override
  30. return FilterInvocation.class.isAssignableFrom(aClass);
  31. }
  32. }

关于自定义这个类,我说如下几点:

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类。

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

关于这个类,我说如下几点:

1.decide方法接收三个参数,其中第一个参数中保存了当前登录用户的角色信息,第三个参数则是UrlFilterInvocationSecurityMetadataSource中的getAttributes方法传来的,表示当前请求需要的角色(可能有多个)。

3.遍历collection,同时查看当前用户的角色列表中是否具备需要的权限,如果具备就直接返回,否则就抛异常。

4.这里涉及到一个all和any的问题:假设当前用户具备角色A、角色B,当前请求需要角色B、角色C,那么是要当前用户要包含所有请求角色才算授权成功还是只要包含一个就算授权成功?我这里采用了第二种方案,即只要包含一个即可。小伙伴可根据自己的实际情况调整decide方法中的逻辑。

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

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

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

关于这个配置,我说如下几点:

1.在configure(HttpSecurity http)方法中,通过withObjectPostProcessor将刚刚创建的UrlFilterInvocationSecurityMetadataSource和UrlAccessDecisionManager注入进来。到时候,请求都会经过刚才的过滤器(除了configure(WebSecurity web)方法忽略的请求)。

2.successHandler中配置登录成功时返回的JSON,登录成功时返回当前用户的信息。

3.failureHandler表示登录失败,登录失败的原因可能有多种,我们根据不同的异常输出不同的错误提示即可。