spring security 自定义

简介

狂神spring security教程没有自定义权限拦截,本文根据[vhr项目](lenve/vhr: 微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。 (github.com))来学习一些自定义的处理。

主要流程

定义entity类

  • UserDetails接口

    • userDetails => Spring Security基础接口,包含某个用户的账号,密码,权限,状态(是否锁定)等信息。只有getter方法。 相当于定义一个规范,作用主要是用来和数据库做交互用的。简单来说,就是用户名传过来,这个类负责校验用户名是否存在业务逻辑。Security这个框架不管你的应用时怎么存储用户和权限信息的。只要你取出来的时候把它包装成一个UserDetails对象给我用就可以了。

    • 源码

      • package org.springframework.security.core.userdetails;
        
        import java.io.Serializable;
        import java.util.Collection;
        import org.springframework.security.core.GrantedAuthority;
        public interface UserDetails extends Serializable {
            //返回用户的所有角色
            Collection<? extends GrantedAuthority> getAuthorities(); 
            String getPassword(); 
            String getUsername();
            //账户是否未过期
            boolean isAccountNonExpired();
            //账户是否未锁定
            boolean isAccountNonLocked(); 
            //凭证是否未过期
            boolean isCredentialsNonExpired();
            //账户是否可用
            boolean isEnabled(); 
        }
        
  • 因此我们定义的entity类要继承UserDetails,使得Spring Security与数据库交互

  • 其中主要重写getAuthorities方法,我们从数据库中读取角色,返回给spring security

  • 例程

  • @Data
    public class Hr implements UserDetails {
        private Integer id;
    
        private String name;
    
        private String phone;
    
        private String telephone;
    
        private String address;
    
        private Boolean enabled;
    
        private String username;
    
        private String password;
    
        private String userface;
    
        private String remark;
        private List<Role> roles;
    
        @Override
        @JsonIgnore
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            return authorities;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    

向spring security提供相关信息

  • 其实认证的操作,框架都已经帮你实现了,它所需要的获取信息的方式。所以它就定义一个接口,然后让你去实现,实现好了之后再注入给它。

  • 框架提供一个UserDetailsService接口用来加载用户信息。UserDetailsService里面只有一个方法,作用就是通过username查询用户的信息。

  • package org.springframework.security.core.userdetails;
    
    public interface UserDetailsService {
        UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
    }
    
  • public interface HrService extends UserDetailsService {
        public List<Hr> getAllHrs(String keywords);
    
        public Integer updateHr(Hr hr);
    
        public boolean updateHrRole(Integer hrid, Integer[] rids);
    
        public Integer deleteHrById(Integer id);
    
        public List<Hr> getAllHrsExceptCurrentHr();
    
        public Integer updateHyById(Hr hr);
    
        public boolean updateHrPasswd(String oldpass, String pass, Integer hrid);
    
        public Integer updateUserface(String url, Integer id);
    }
    
  • @Service
    public class HrServiceImpl implements HrService {
        @Autowired
        HrMapper hrMapper;
        @Autowired
        HrRoleMapper hrRoleMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            Hr hr = hrMapper.loadUserByUsername(username);
            if (hr == null) {
                throw new UsernameNotFoundException("用户名不存在!");
            }
            hr.setRoles(hrMapper.getHrRolesById(hr.getId()));
            return hr;
        }
    
        public List<Hr> getAllHrs(String keywords) {
            return hrMapper.getAllHrs(HrUtils.getCurrentHr().getId(),keywords);
        }
    
        public Integer updateHr(Hr hr) {
            return hrMapper.updateByPrimaryKeySelective(hr);
        }
    
        @Transactional
        public boolean updateHrRole(Integer hrid, Integer[] rids) {
            hrRoleMapper.deleteByHrid(hrid);
            return hrRoleMapper.addRole(hrid, rids) == rids.length;
        }
    
        public Integer deleteHrById(Integer id) {
            return hrMapper.deleteByPrimaryKey(id);
        }
    
        public List<Hr> getAllHrsExceptCurrentHr() {
            return hrMapper.getAllHrsExceptCurrentHr(HrUtils.getCurrentHr().getId());
        }
    
        public Integer updateHyById(Hr hr) {
            return hrMapper.updateByPrimaryKeySelective(hr);
        }
    
        public boolean updateHrPasswd(String oldpass, String pass, Integer hrid) {
            Hr hr = hrMapper.selectByPrimaryKey(hrid);
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            if (encoder.matches(oldpass, hr.getPassword())) {
                String encodePass = encoder.encode(pass);
                Integer result = hrMapper.updatePasswd(hrid, encodePass);
                if (result == 1) {
                    return true;
                }
            }
            return false;
        }
    
        public Integer updateUserface(String url, Integer id) {
            return hrMapper.updateUserface(url, id);
        }
    }
    
  • 再把UserDetailsService提供给spring security(同时对密码加密)

  •     @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(hrService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
  • 现在就可以读取数据库中相关的信息

自定义权限拦截

获取URL资源需要的角色

  • 自定义FilterInvocationSecurityMetadataSource

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

    • @Component
      public class UrlFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
          //资源表
          @Autowired
          MenuService menuService;
          AntPathMatcher antPathMatcher = new AntPathMatcher();
          @Override
          public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
              // 获取当前请求url
              String requestUrl = ((FilterInvocation) object).getRequestUrl();
              // TODO 忽略url请放在此处进行过滤放行
              if ("/login".equals(requestUrl)) {
                  return null;
              }
              // 数据库中所有url
              java.util.List<Menu> menus = menuService.getAllMenusWithRole();
              for (Menu menu : menus) {
                  // 获取该url所对应的权限
                  //使用antPathMatcher进行路径匹配
                  if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                      List<Role> roles = menu.getRoles();
                      String[] str = new String[roles.size()];
                      for (int i = 0; i < roles.size(); i++) {
                          str[i] = roles.get(i).getName();
                      }
                      // 保存该url对应角色权限信息
                      return SecurityConfig.createList(str);
                  }
              }
              //ROLE_LOGIN在角色数据库中并不存在,后面单独处理
              return SecurityConfig.createList("ROLE_LOGIN");
          }
      
          @Override
          public Collection<ConfigAttribute> getAllConfigAttributes() {
              return null;
          }
      
          @Override
          public boolean supports(Class<?> clazz) {
              return true;
          }
      }
      

验证用户的权限

  • 自定义AccessDecisionManager来验证用户的权限

  • @Component
    public class UrlAccessDecisionManager implements AccessDecisionManager {
        /**
         *
         * @param authentication  保存了当前登录用户的角色信息
         * @param o
         * @param collection  当前请求需要的角色(可能有多个)
         * @throws AccessDeniedException
         * @throws AuthenticationException
         */
        @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();
                //ROLE_LOGIN单独处理
                if ("ROLE_LOGIN".equals(needRole)) {
                    if (authentication instanceof AnonymousAuthenticationToken) {
                        //重新登录,给AuthenticationEntryPoint处理
                        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;
        }
    }
    

自定义权限不足处理

  • @Component
    public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
        //响应403错误
        @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();
        }
    }
    
  • accessDeniedHandler:访问拒绝处理 就是你要访问某个资源,但是当你没有访问权限时,就会抛出异常,在此类中进行处理。

spring security配置

  • 登录界面使用spring security自带的,当然可以自己定制,在formLogin里面配置

  • 配置登录成功

  • 配置登录失败

  • 配置加密方式

  • 配置UserDetailsService

  • 配置自定义的AccessDecisionManager与FilterInvocationSecurityMetadataSource

  • @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        HrService hrService;
        @Autowired
        UrlFilterInvocationSecurityMetadataSource urlFilterInvocationSecurityMetadataSource;
        @Autowired
        UrlAccessDecisionManager urlAccessDecisionManager;
        @Autowired
        AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;
    
    //    @Bean
    //    public PasswordEncoder passwordEncoder() {
    //        //为了演示方便,我们使用NoOpPasswordEncoder(这个就是不加密)
    //        return NoOpPasswordEncoder.getInstance();
    //    }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(hrService).passwordEncoder(new BCryptPasswordEncoder());
        }
    
    
        //配置资源放行,不登录就可以访问
        @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().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(HrUtils.getCurrentHr()) + "}";
                            out.write(s);
                            out.flush();
                            out.close();
                        }
                    }).and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler);
        }
    }
    
posted @ 2021-09-24 11:13  貂蝉贼6  阅读(229)  评论(0)    收藏  举报