{"id":13458883,"url":"https://github.com/yangxiufeng666/Micro-Service-Skeleton","last_synced_at":"2025-03-24T16:31:05.441Z","repository":{"id":65574565,"uuid":"113426268","full_name":"yangxiufeng666/Micro-Service-Skeleton","owner":"yangxiufeng666","description":"微服务开发基础框架","archived":false,"fork":false,"pushed_at":"2024-03-15T14:26:48.000Z","size":249,"stargazers_count":850,"open_issues_count":23,"forks_count":536,"subscribers_count":77,"default_branch":"master","last_synced_at":"2024-10-29T04:34:26.494Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yangxiufeng666.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-12-07T08:55:57.000Z","updated_at":"2024-09-29T00:08:54.000Z","dependencies_parsed_at":"2024-07-31T09:10:03.859Z","dependency_job_id":"539e8548-9e19-44e2-a862-8f085ba38a04","html_url":"https://github.com/yangxiufeng666/Micro-Service-Skeleton","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yangxiufeng666%2FMicro-Service-Skeleton","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yangxiufeng666%2FMicro-Service-Skeleton/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yangxiufeng666%2FMicro-Service-Skeleton/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yangxiufeng666%2FMicro-Service-Skeleton/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yangxiufeng666","download_url":"https://codeload.github.com/yangxiufeng666/Micro-Service-Skeleton/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245308471,"owners_count":20594257,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T09:00:59.278Z","updated_at":"2025-03-24T16:31:05.067Z","avatar_url":"https://github.com/yangxiufeng666.png","language":"Java","funding_links":[],"categories":["Java"],"sub_categories":[],"readme":"# Micro-Service-Skeleton\n微服务开发基础框架\n\n# 版本说明\n目前使用的Nacos版本，如需Eureka作为注册中心的请采用tags v2.0(https://github.com/babylikebird/Micro-Service-Skeleton/tree/v2.0)\n\n\n目前已经改版，进阶版：https://blog.csdn.net/w1054993544/article/details/109361170\n\n## 一、需求\n在2018年写的[基于OAUTH2.0统一认证授权的微服务基础架构](https://blog.csdn.net/w1054993544/article/details/78932614)只是基于OAUTH认证授权的入门级应用。本文基于实战目的，实现权限的动态控制。\n现有如下需求：\n\n 1. 基于用户-角色-权限控制\n 2. 权限粒度控制到具体的请求URL\n 3. 当用户的角色或者权限变动后，已获授权的用户需要重新登录授权\n\n本文围绕上面三个基本需求进行实现。\n\n## 二、工程说明\n设计的框架已经中间件有：\n 1. Nacos 1.3\n 2. Spring Cloud Hoxton.SR8\n 3. JWT  nimbus-jose-jwt\n 4. Spring Cloud Gateway\n 5. Spring security\n 6. mybatis-plus\n 7. Redis\n 8. mysql\n\n设计的主要工程有：\n\n 1. gateway：网关，动态权限判断\n 2. auth：认证中心\n 3. upms：用户权限管理服务\n \n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201029154424469.png#pic_center)\n## 三、认证中心\n### 3.1 WebSecurityConfig\n登录认证授权等主要采用Spring security + JWT，那么得首先配置WebSecurityConfig，这里看到的redis配置主要是为了满足需求点3（当用户的角色或者权限变动后，已获授权的用户需要重新登录授权）\n\n```java\npublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {\n\n    @Autowired\n    @Qualifier(\"userDetailsServiceImpl\")\n    private UserDetailsService userDetailsService;\n\n    @Autowired\n    private StringRedisTemplate redisTemplate;\n\n    @Bean\n    public PasswordEncoder passwordEncoder() {\n        return new BCryptPasswordEncoder();\n    }\n\n    @Override\n    protected void configure(AuthenticationManagerBuilder auth) throws Exception {\n        auth.userDetailsService(userDetailsService)\n            .passwordEncoder(passwordEncoder());\n    }\n\n    @Override\n    protected void configure(HttpSecurity http) throws Exception {\n        http.csrf().disable()\n                .authorizeRequests()\n                .antMatchers(HttpMethod.POST, \"/login\").permitAll()\n                .anyRequest().authenticated()\n                .and()\n                .addFilterBefore(new JWTAuthenticationFilter(authenticationManager() , redisTemplate),  UsernamePasswordAuthenticationFilter.class)\n                .addFilterBefore(new JWTAuthorizationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)\n                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)\n                .and()\n                .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());\n    }\n\n\n}\n```\n### 3.2 UserDetailsServiceImpl\n通过用户名去查找用户及拥有的角色和权限\n```java\npublic class UserDetailsServiceImpl implements UserDetailsService {\n    @Autowired\n    private UpmsService upmsService;\n\n    @Override\n    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {\n        Result\u003cUserVo\u003e userResult = upmsService.findByUsername(username);\n        if (userResult.getCode() != StatusCode.SUCCESS_CODE) {\n            throw new UsernameNotFoundException(\"用户:\" + username + \",不存在!\");\n        }\n        Set\u003cSimpleGrantedAuthority\u003e grantedAuthorities = new HashSet\u003c\u003e();\n        UserVo userVo = new UserVo();\n        BeanUtils.copyProperties(userResult.getData(),userVo);\n        Result\u003cList\u003cRoleVo\u003e\u003e roleResult = upmsService.getRoleByUserId(userVo.getId());\n        if (roleResult.getCode() == StatusCode.SUCCESS_CODE){\n            List\u003cRoleVo\u003e roleVoList = roleResult.getData();\n            for (RoleVo role:roleVoList){\n                //角色必须是ROLE_开头，可以在数据库中设置\n                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(\"ROLE_\"+role.getValue());\n                grantedAuthorities.add(grantedAuthority);\n                //获取权限\n                Result\u003cList\u003cMenuVo\u003e\u003e perResult  = upmsService.getRolePermission(role.getId());\n                if (perResult.getCode() == StatusCode.SUCCESS_CODE){\n                    List\u003cMenuVo\u003e permissionList = perResult.getData();\n                    for (MenuVo menu:permissionList\n                            ) {\n                        if ( !StringUtils.isEmpty(menu.getUrl()) ){\n                            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(menu.getUrl());\n                            grantedAuthorities.add(authority);\n                        }\n                    }\n                }\n            }\n        }\n        AuthUser user = new AuthUser(userVo.getUsername(), userVo.getPassword(), grantedAuthorities);\n        user.setId(userVo.getId());\n        return user;\n    }\n}\n```\n### 3.3 JWTAuthenticationFilter\n主要对用户进行认证工作，当登录时，获取用户名和密码，通过authenticationManager.authenticate，最终会调用UserDetailsServiceImpl来获取用户信息(在DaoAuthenticationProvider的retrieveUser中)，\n然后在DaoAuthenticationProvider的additionalAuthenticationChecks中校验密码，这里不再对spring security的细节进行更多的赘述，读者可直接看它源码。\n\n```java\npublic class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {\n\n    /**\n     * 过期时间2小时\n     */\n    private static final long EXPIRE_TIME = 1000 * 60 * 60 * 2;\n\n    private AuthenticationManager authenticationManager;\n\n    private StringRedisTemplate redisTemplate;\n\n    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, StringRedisTemplate redisTemplate) {\n        this.authenticationManager = authenticationManager;\n        this.redisTemplate = redisTemplate;\n    }\n\n    @Override\n    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {\n        String username = request.getParameter(\"username\");\n        String password = request.getParameter(\"password\");\n        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));\n    }\n\n    @SneakyThrows\n    @Override\n    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {\n        AuthUser user = (AuthUser) authResult.getPrincipal();\n        /**\n         * 1、创建密钥\n         */\n        MACSigner macSigner = new MACSigner(JWTConstants.SECRET);\n        /**\n         * 2、payload\n         */\n        String payload = JSONObject.toJSONString(user);\n        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()\n                .subject(\"subject\")\n                .claim(\"payload\", payload)\n                .expirationTime(new Date(System.currentTimeMillis() + EXPIRE_TIME))\n                .build();\n        JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);\n        /**\n         * 创建签名的JWT\n         */\n        SignedJWT signedJWT = new SignedJWT(jwsHeader , claimsSet);\n        signedJWT.sign(macSigner);\n        /**\n         * 生成token\n         */\n        String jwtToken = signedJWT.serialize();\n        response.setContentType(MediaType.APPLICATION_JSON_VALUE);\n        response.setCharacterEncoding(\"UTF-8\");\n        Result result = Result.ok().setData(jwtToken);\n        //生成Key， 把权限放入到redis中\n        String keyPrefix = \"JWT\" + user.getId() + \":\";\n        String keySuffix = Md5Utils.getMD5(jwtToken.getBytes());\n        String key = keyPrefix + keySuffix;\n\n        String authKey = key + \":Authorities\";\n\n        redisTemplate.opsForValue().set(key , jwtToken , EXPIRE_TIME , TimeUnit.MILLISECONDS);\n\n        redisTemplate.opsForValue().set(authKey, JSONObject.toJSONString(user.getAuthorities()), EXPIRE_TIME , TimeUnit.SECONDS);\n\n        response.getWriter().write(JSONObject.toJSONString(result));\n    }\n\n    @Override\n    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {\n        log.error(\"登录认证失败\",failed);\n        Result result = null;\n        int status = 401;\n        if (failed instanceof UsernameNotFoundException){\n            result = Result.failure(404, \"用户不存在\");\n        }else if (failed instanceof BadCredentialsException){\n            result = Result.failure(401, \"用户名密码错误\");\n        }\n        response.setContentType(MediaType.APPLICATION_JSON_VALUE);\n        response.setCharacterEncoding(\"UTF-8\");\n        response.setStatus(status);\n        response.getWriter().write(JSONObject.toJSONString(result));\n    }\n```\n\n主要是生成JWT的token， 并且把权限信息放入redis。\n\n## 四、 Gateway 网关\n网关的主要作用是对JWT和具体的URL进行校验，校验不通过则返回错误信息。主要通过AuthFilter来实现，定义的AuthFilter如下：\n\n```java\n@Component\n@Slf4j\npublic class AuthFilter implements GlobalFilter , Ordered {\n\n    @Autowired\n    private ExclusionUrl exclusionUrl;\n\n    AntPathMatcher antPathMatcher = new AntPathMatcher();\n\n    @Autowired\n    private StringRedisTemplate redisTemplate;\n\n    @Override\n    public Mono\u003cVoid\u003e filter(ServerWebExchange exchange, GatewayFilterChain chain) {\n        ServerHttpRequest request = exchange.getRequest();\n        ServerHttpResponse response = exchange.getResponse();\n        String headerToken = request.getHeaders().getFirst(JWTConstants.TOKEN_HEADER);\n        log.info(\"headerToken:{}\", headerToken);\n        //1、只要带上了token， 就需要判断Token是否有效\n        if ( !StringUtils.isEmpty(headerToken) \u0026\u0026 !verifierToken(headerToken)){\n            return getVoidMono(response, 401, \"token无效\");\n        }\n        String path = request.getURI().getPath();\n        log.info(\"request path:{}\", path);\n        //2、判断是否是过滤的路径， 是的话就放行\n        if ( isExclusionUrl(path) ){\n            return chain.filter(exchange);\n        }\n        //3、判断请求的URL是否有权限\n        boolean permission = hasPermission(headerToken , path);\n        if (!permission){\n            return getVoidMono(response, 403, \"无访问权限\");\n        }\n        return chain.filter(exchange);\n    }\n\n\n    @Override\n    public int getOrder() {\n        return 0;\n    }\n\n    private Mono\u003cVoid\u003e getVoidMono(ServerHttpResponse response, int i, String msg) {\n        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);\n        response.setStatusCode(HttpStatus.OK);\n        Result failed = Result.failure(i, msg);\n        byte[] bits = JSON.toJSONString(failed).getBytes();\n        DataBuffer buffer = response.bufferFactory().wrap(bits);\n        return response.writeWith(Mono.just(buffer));\n    }\n\n    private boolean isExclusionUrl(String path){\n        List\u003cString\u003e exclusions = exclusionUrl.getUrl();\n        if (exclusions.size() == 0){\n            return false;\n        }\n        return exclusions.stream().anyMatch( action -\u003e antPathMatcher.match(action , path));\n\n    }\n\n    private boolean verifierToken(String headerToken){\n        try {\n            SignedJWT jwt = getSignedJWT(headerToken);\n            JWSVerifier verifier = new MACVerifier(JWTConstants.SECRET);\n            //校验是否有效\n            if (!jwt.verify(verifier)) {\n                log.error(\"token不合法，检测不过关\");\n                return false;\n            }\n            //校验超时\n            Date expirationTime = jwt.getJWTClaimsSet().getExpirationTime();\n            if (new Date().after(expirationTime)) {\n                log.error(\"token已经过期\");\n                return false;\n            }\n            //获取载体中的数据\n            return true;\n        } catch (ParseException | JOSEException e) {\n            log.error(\"token校验出错\",e);\n        }\n        return false;\n    }\n    private boolean hasPermission(String headerToken, String path){\n        try {\n            if (StringUtils.isEmpty(headerToken)){\n                return false;\n            }\n            SignedJWT jwt = getSignedJWT(headerToken);\n            Object payload = jwt.getJWTClaimsSet().getClaim(\"payload\");\n            UserVo user = JSON.parseObject(payload.toString(), UserVo.class);\n            //生成Key， 把权限放入到redis中\n            String keyPrefix = \"JWT\" + user.getId() + \":\";\n            String token = headerToken.replace(JWTConstants.TOKEN_PREFIX, \"\");\n            String keySuffix = Md5Utils.getMD5(token.getBytes());\n            String key = keyPrefix + keySuffix;\n            String authKey = key + \":Authorities\";\n\n            String authStr = redisTemplate.opsForValue().get(authKey);\n            if (StringUtils.isEmpty(authStr)){\n                return false;\n            }\n            List\u003cAuthority\u003e authorities = JSON.parseArray(authStr , Authority.class);\n            return authorities.stream().anyMatch(authority -\u003e antPathMatcher.match(authority.getAuthority(), path));\n        } catch (ParseException e) {\n            e.printStackTrace();\n        }\n        return false;\n    }\n    private SignedJWT getSignedJWT(String headerToken) throws ParseException {\n        String token = headerToken.replace(JWTConstants.TOKEN_PREFIX, \"\");\n        log.info(\"token is {}\", token);\n        return SignedJWT.parse(token);\n    }\n}\n```\n\n## 五、运行效果\n我们现在数据库中添加几个菜单（**这里为了简单直接在数据库添加**）\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201030090956767.png#pic_center)\n\n添加角色：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201029162631289.png#pic_center)\n给角色赋予权限\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201029162651252.png#pic_center)\n\n给用户赋予角色：\n\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201029162719119.png#pic_center)\n现在用admin的用户登录等到token：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201029162850178.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3cxMDU0OTkzNTQ0,size_16,color_FFFFFF,t_70#pic_center)\n用Token去访问order/list，看到这里返回order list， 权限认证成功。\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201030091042380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3cxMDU0OTkzNTQ0,size_16,color_FFFFFF,t_70#pic_center)\n\n\n如访问user/findByUsername这会提示无权限，这样就实现到具体URL的鉴权了。\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201030091130822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3cxMDU0OTkzNTQ0,size_16,color_FFFFFF,t_70#pic_center)\n\n## 六、权限变动重新授权\n我们在JWTAuthenticationFilter中，把权限信息等写入到了redis中，key的规则如下\n\n```java\n        //生成Key， 把权限放入到redis中\n        String keyPrefix = \"JWT\" + user.getId() + \":\";\n        String keySuffix = Md5Utils.getMD5(jwtToken.getBytes());\n        String key = keyPrefix + keySuffix;\n\n        String authKey = key + \":Authorities\";\n```\nredis中看到如下：\n![在这里插入图片描述](https://img-blog.csdnimg.cn/20201029164153972.png#pic_center)\n只要后台权限变动的时候，根据key的规则清除redis数据即可， 然后在gateway中获取不到相应的权限， 那么会要求用户重新登录。\n\n到这里我们已经实现了第一节提的3个需求， 更多的玩法，由各位自己发挥了。\n\n[代码在这里哟](https://github.com/babylikebird/Micro-Service-Skeleton)\n[重要事情再说一遍](https://github.com/babylikebird/Micro-Service-Skeleton)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyangxiufeng666%2FMicro-Service-Skeleton","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyangxiufeng666%2FMicro-Service-Skeleton","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyangxiufeng666%2FMicro-Service-Skeleton/lists"}