• SpringBoot整合SpringSecurityOauth2实现鉴权-动态权限


    写在前面

    思考:为什么需要鉴权呢?

    系统开发好上线后,API接口会暴露在互联网上会存在一定的安全风险,例如:爬虫、恶意访问等。因此,我们需要对非开放API接口进行用户鉴权,鉴权通过之后再允许调用。

     

    准备

    spring-boot:2.1.4.RELEASE

    spring-security-oauth2:2.3.3.RELEASE(如果要使用源码,不要随意改动这个版本号,因为2.4往上的写法不一样了)

    mysql:5.7

     

    效果展示

    这边只用了postman做测试,暂时未使用前端页面来对接,下个版本角色菜单权限分配的会有页面的展示

     

    1、访问开放接口 http://localhost:7000/open/hello

     

     

    2、不带token访问受保护接口 http://localhost:7000/admin/user/info

     

     

    3、登录后获取token,带上token访问,成功返回了当前的登录用户信息

     

     

     

     

    实现

    oauth2一共有四种模式,这边就不做讲解了,网上搜一搜,千篇一律

    因为现在只考虑做单方应用的,所以使用的是密码模式。

    后面会出一篇SpringCloud+Oauth2的文章,网关鉴权

     

    讲一下几个点吧

    1、拦截器配置动态权限

     

    新建一个 MySecurityFilter类,继承AbstractSecurityInterceptor,并实现Filter接口

     初始化,自定义访问决策管理器

    @PostConstruct
     public void init(){
            super.setAuthenticationManager(authenticationManager);
            super.setAccessDecisionManager(myAccessDecisionManager);
      }   

     

    自定义 过滤器调用安全元数据源

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return this.mySecurityMetadataSource;
    }

     

     

    先来看一下自定义过滤器调用安全元数据源的核心代码

    以下代码是用来获取到当前请求进来所需要的权限(角色)

     

    复制代码
    /**
         * 获得当前请求所需要的角色
         * @param object
         * @return
         * @throws IllegalArgumentException
         */
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            String requestUrl = ((FilterInvocation) object).getRequestUrl();
    
            if (IS_CHANGE_SECURITY) {
                loadResourceDefine();
            }
            if (requestUrl.indexOf("?") > -1) {
                requestUrl = requestUrl.substring(0, requestUrl.indexOf("?"));
            }
            UrlPathMatcher matcher = new UrlPathMatcher();
            List<Object> list = new ArrayList<>();  //无需权限的,直接返回
            list.add("/oauth/**");
            list.add("/open/**");
            if(matcher.pathsMatchesUrl(list,requestUrl))
                return null;
    
            Set<String> roleNames = new HashSet();
            for (Resc resc: resources) {
                String rescUrl = resc.getResc_url();
                if (matcher.pathMatchesUrl(rescUrl, requestUrl)) {
                    if(resc.getParent_resc_id() != null && resc.getParent_resc_id().intValue() == 1){   //默认权限的则只要登录了,无需权限匹配都可访问
                        roleNames = new HashSet();
                        break;
                    }
                    Map map = new HashMap();
                    map.put("resc_id", resc.getResc_id());
                    // 获取能访问该资源的所有权限(角色)
                    List<RoleRescDTO> roles = roleRescMapper.findAll(map);
                    for (RoleRescDTO rr : roles)
                        roleNames.add(rr.getRole_name());
                }
            }
    
            Set<ConfigAttribute> configAttributes = new HashSet();
            for(String roleName:roleNames)
                configAttributes.add(new SecurityConfig(roleName));
    
            log.debug("【所需的权限(角色)】:" + configAttributes);
    
            return configAttributes;
        }
    复制代码

     

    再来看一下自定义访问决策管理器核心代码,这段代码主要是判断当前登录用户(当前登录用户所拥有的角色会在最后一项写到)是否拥有该权限角色

    复制代码
    @Override
        public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            if(configAttributes == null){   //属于白名单的,不需要权限
                return;
            }
            Iterator<ConfigAttribute> iterator = configAttributes.iterator();
            while (iterator.hasNext()){
                ConfigAttribute configAttribute = iterator.next();
                String needPermission = configAttribute.getAttribute();
                for (GrantedAuthority ga: authentication.getAuthorities()) {
                    if(needPermission.equals(ga.getAuthority())){   //有权限,可访问
                        return;
                    }
                }
            }
            throw new AccessDeniedException("没有权限访问");
    
        }
    复制代码

     

    2、自定义鉴权异常返回通用结果

    为什么需要这个呢,如果不配置这个,对于前端,后端来说都很难去理解鉴权失败返回的内容,还不能统一解读,废话不多说,先看看不配置和配置了的返回情况

    (1)未自定义前,没有携带token去访问受保护的API接口时,返回的结果是这样的

     

     (2)我们规定一下,鉴权失败的接口返回接口之后,变成下面这种了,是不是更利于我们处理和提示用户

     

     

     

    好了,来看一下是在哪里去配置的吧

    我们资源服务器OautyResourceConfig,重写下下面这部分的代码,来自定义鉴权异常返回的结果

    大伙可以参考下这个 https://blog.csdn.net/Pastxu/article/details/124538364

    @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.authenticationEntryPoint(authenticationEntryPoint)    //token失效或没携带token时
                    .accessDeniedHandler(requestAccessDeniedHandler);   //权限不足时
        }

     

     

    3、获取当前登录用户

    第一种:使用JWT携带用户信息,拿到token后再解析

    暂不做解释

    第二种:写一个SecurityUser实现UserDetails接口(这个工程中使用的是这一种)

    原来的只有UserDetails接口只有username和password,这里我们加上我们系统中的User

    复制代码
    protected User user;
        public SecurityUser(User user) {
            this.user = user;
        }
    
        public User getUser() {
            return user;
        }
    复制代码

     

    在BaseController,每个Controller都会继承这个的,在里面写给getUser()的方法,只要用户带了token来访问,我们可以直接获取当前登录用户的信息了

    复制代码
    protected User getUser() {
            try {
                SecurityUser userDetails = (SecurityUser) SecurityContextHolder.getContext().getAuthentication()
                        .getPrincipal();
    
                User user = userDetails.getUser();
                log.debug("【用户:】:" + user);
    
                return user;
            } catch (Exception e) {
            }
            return null;
        }
    复制代码

     

    那么用户登录成功后,如何去拿到用户的角色集合等呢,这里面就要实现UserDetailsService接口了

     

    复制代码
    @Service
    public class TokenUserDetailsService implements UserDetailsService{
    
        @Autowired
        private LoginService loginService;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = loginService.loadUserByUsername(username);  //这个我们拎出来处理
            if(Objects.isNull(user))
                throw new UsernameNotFoundException("用户名不存在");
            return new SecurityUser(user);
        }
    }
    复制代码

     

    然后在我们的安全配置类中设置UserDetailsService为上面的我们自己写的就行

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

     

    最后我们只需要在loginService里面实现我们的方法就好,根据我们的实际业务处理判断该用户是否存在等

    复制代码
    @Override
        public User loadUserByUsername(String username){
            log.debug(username);
            Map map = new HashMap();
            map.put("username",username);
            map.put("is_deleted",-1);
            User user = userMapper.findByUsername(map);
            if(user != null){
                map = new HashMap();
                map.put("user_id",user.getUser_id());
                //查询用户的角色
                List<UserRoleDTO> userRoles = userRoleMapper.findAll(map);
                user.setRoles(listRoles(userRoles));
                //权限集合
                Collection<? extends GrantedAuthority> authorities = merge(userRoles);
                user.setAuthorities(authorities);
                return user;
            }
            return null;
    
        }
    复制代码

     

     

    大功告成啦,赶紧动起手来吧!

    附上源码地址:https://gitee.com/jae_1995/spring-boot-oauth2

    数据库文件在这

     

     

     

    点个小赞呗

     

  • 相关阅读:
    2024全国水科技大会:【协办单位】山东文远环保科技股份有限公司
    sa-token学习
    Python实现Welch‘s T-test
    【多卡训练报错】:The server socket has failed to listen on any local network address.
    Docker方式创建MySQL8的MGR集群
    C++ 学习(19)STL - list容器、set容器
    面向对象之抽象类的认识 - (java语法)
    [python]pytest运行报错pytest: error: unrecognized arguments: --reruns
    【Mysql】Mysql中的B+树索引(六)
    this指向
  • 原文地址:https://www.cnblogs.com/jae-tech/p/16381881.html