Coder Social home page Coder Social logo

micro-service-skeleton's Introduction

Micro-Service-Skeleton

微服务开发基础框架

版本说明

目前使用的Nacos版本,如需Eureka作为注册中心的请采用tags v2.0(https://github.com/babylikebird/Micro-Service-Skeleton/tree/v2.0)

目前已经改版,进阶版:https://blog.csdn.net/w1054993544/article/details/109361170

一、需求

在2018年写的基于OAUTH2.0统一认证授权的微服务基础架构只是基于OAUTH认证授权的入门级应用。本文基于实战目的,实现权限的动态控制。 现有如下需求:

  1. 基于用户-角色-权限控制
  2. 权限粒度控制到具体的请求URL
  3. 当用户的角色或者权限变动后,已获授权的用户需要重新登录授权

本文围绕上面三个基本需求进行实现。

二、工程说明

设计的框架已经中间件有:

  1. Nacos 1.3
  2. Spring Cloud Hoxton.SR8
  3. JWT nimbus-jose-jwt
  4. Spring Cloud Gateway
  5. Spring security
  6. mybatis-plus
  7. Redis
  8. mysql

设计的主要工程有:

  1. gateway:网关,动态权限判断
  2. auth:认证中心
  3. upms:用户权限管理服务

在这里插入图片描述

三、认证中心

3.1 WebSecurityConfig

登录认证授权等主要采用Spring security + JWT,那么得首先配置WebSecurityConfig,这里看到的redis配置主要是为了满足需求点3(当用户的角色或者权限变动后,已获授权的用户需要重新登录授权)

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JWTAuthenticationFilter(authenticationManager() , redisTemplate),  UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JWTAuthorizationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
    }


}

3.2 UserDetailsServiceImpl

通过用户名去查找用户及拥有的角色和权限

public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UpmsService upmsService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Result<UserVo> userResult = upmsService.findByUsername(username);
        if (userResult.getCode() != StatusCode.SUCCESS_CODE) {
            throw new UsernameNotFoundException("用户:" + username + ",不存在!");
        }
        Set<SimpleGrantedAuthority> grantedAuthorities = new HashSet<>();
        UserVo userVo = new UserVo();
        BeanUtils.copyProperties(userResult.getData(),userVo);
        Result<List<RoleVo>> roleResult = upmsService.getRoleByUserId(userVo.getId());
        if (roleResult.getCode() == StatusCode.SUCCESS_CODE){
            List<RoleVo> roleVoList = roleResult.getData();
            for (RoleVo role:roleVoList){
                //角色必须是ROLE_开头,可以在数据库中设置
                SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+role.getValue());
                grantedAuthorities.add(grantedAuthority);
                //获取权限
                Result<List<MenuVo>> perResult  = upmsService.getRolePermission(role.getId());
                if (perResult.getCode() == StatusCode.SUCCESS_CODE){
                    List<MenuVo> permissionList = perResult.getData();
                    for (MenuVo menu:permissionList
                            ) {
                        if ( !StringUtils.isEmpty(menu.getUrl()) ){
                            SimpleGrantedAuthority authority = new SimpleGrantedAuthority(menu.getUrl());
                            grantedAuthorities.add(authority);
                        }
                    }
                }
            }
        }
        AuthUser user = new AuthUser(userVo.getUsername(), userVo.getPassword(), grantedAuthorities);
        user.setId(userVo.getId());
        return user;
    }
}

3.3 JWTAuthenticationFilter

主要对用户进行认证工作,当登录时,获取用户名和密码,通过authenticationManager.authenticate,最终会调用UserDetailsServiceImpl来获取用户信息(在DaoAuthenticationProvider的retrieveUser中), 然后在DaoAuthenticationProvider的additionalAuthenticationChecks中校验密码,这里不再对spring security的细节进行更多的赘述,读者可直接看它源码。

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    /**
     * 过期时间2小时
     */
    private static final long EXPIRE_TIME = 1000 * 60 * 60 * 2;

    private AuthenticationManager authenticationManager;

    private StringRedisTemplate redisTemplate;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, StringRedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    @SneakyThrows
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        AuthUser user = (AuthUser) authResult.getPrincipal();
        /**
         * 1、创建密钥
         */
        MACSigner macSigner = new MACSigner(JWTConstants.SECRET);
        /**
         * 2、payload
         */
        String payload = JSONObject.toJSONString(user);
        JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
                .subject("subject")
                .claim("payload", payload)
                .expirationTime(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                .build();
        JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
        /**
         * 创建签名的JWT
         */
        SignedJWT signedJWT = new SignedJWT(jwsHeader , claimsSet);
        signedJWT.sign(macSigner);
        /**
         * 生成token
         */
        String jwtToken = signedJWT.serialize();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        Result result = Result.ok().setData(jwtToken);
        //生成Key, 把权限放入到redis中
        String keyPrefix = "JWT" + user.getId() + ":";
        String keySuffix = Md5Utils.getMD5(jwtToken.getBytes());
        String key = keyPrefix + keySuffix;

        String authKey = key + ":Authorities";

        redisTemplate.opsForValue().set(key , jwtToken , EXPIRE_TIME , TimeUnit.MILLISECONDS);

        redisTemplate.opsForValue().set(authKey, JSONObject.toJSONString(user.getAuthorities()), EXPIRE_TIME , TimeUnit.SECONDS);

        response.getWriter().write(JSONObject.toJSONString(result));
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.error("登录认证失败",failed);
        Result result = null;
        int status = 401;
        if (failed instanceof UsernameNotFoundException){
            result = Result.failure(404, "用户不存在");
        }else if (failed instanceof BadCredentialsException){
            result = Result.failure(401, "用户名密码错误");
        }
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setStatus(status);
        response.getWriter().write(JSONObject.toJSONString(result));
    }

主要是生成JWT的token, 并且把权限信息放入redis。

四、 Gateway 网关

网关的主要作用是对JWT和具体的URL进行校验,校验不通过则返回错误信息。主要通过AuthFilter来实现,定义的AuthFilter如下:

@Component
@Slf4j
public class AuthFilter implements GlobalFilter , Ordered {

    @Autowired
    private ExclusionUrl exclusionUrl;

    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String headerToken = request.getHeaders().getFirst(JWTConstants.TOKEN_HEADER);
        log.info("headerToken:{}", headerToken);
        //1、只要带上了token, 就需要判断Token是否有效
        if ( !StringUtils.isEmpty(headerToken) && !verifierToken(headerToken)){
            return getVoidMono(response, 401, "token无效");
        }
        String path = request.getURI().getPath();
        log.info("request path:{}", path);
        //2、判断是否是过滤的路径, 是的话就放行
        if ( isExclusionUrl(path) ){
            return chain.filter(exchange);
        }
        //3、判断请求的URL是否有权限
        boolean permission = hasPermission(headerToken , path);
        if (!permission){
            return getVoidMono(response, 403, "无访问权限");
        }
        return chain.filter(exchange);
    }


    @Override
    public int getOrder() {
        return 0;
    }

    private Mono<Void> getVoidMono(ServerHttpResponse response, int i, String msg) {
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(HttpStatus.OK);
        Result failed = Result.failure(i, msg);
        byte[] bits = JSON.toJSONString(failed).getBytes();
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        return response.writeWith(Mono.just(buffer));
    }

    private boolean isExclusionUrl(String path){
        List<String> exclusions = exclusionUrl.getUrl();
        if (exclusions.size() == 0){
            return false;
        }
        return exclusions.stream().anyMatch( action -> antPathMatcher.match(action , path));

    }

    private boolean verifierToken(String headerToken){
        try {
            SignedJWT jwt = getSignedJWT(headerToken);
            JWSVerifier verifier = new MACVerifier(JWTConstants.SECRET);
            //校验是否有效
            if (!jwt.verify(verifier)) {
                log.error("token不合法,检测不过关");
                return false;
            }
            //校验超时
            Date expirationTime = jwt.getJWTClaimsSet().getExpirationTime();
            if (new Date().after(expirationTime)) {
                log.error("token已经过期");
                return false;
            }
            //获取载体中的数据
            return true;
        } catch (ParseException | JOSEException e) {
            log.error("token校验出错",e);
        }
        return false;
    }
    private boolean hasPermission(String headerToken, String path){
        try {
            if (StringUtils.isEmpty(headerToken)){
                return false;
            }
            SignedJWT jwt = getSignedJWT(headerToken);
            Object payload = jwt.getJWTClaimsSet().getClaim("payload");
            UserVo user = JSON.parseObject(payload.toString(), UserVo.class);
            //生成Key, 把权限放入到redis中
            String keyPrefix = "JWT" + user.getId() + ":";
            String token = headerToken.replace(JWTConstants.TOKEN_PREFIX, "");
            String keySuffix = Md5Utils.getMD5(token.getBytes());
            String key = keyPrefix + keySuffix;
            String authKey = key + ":Authorities";

            String authStr = redisTemplate.opsForValue().get(authKey);
            if (StringUtils.isEmpty(authStr)){
                return false;
            }
            List<Authority> authorities = JSON.parseArray(authStr , Authority.class);
            return authorities.stream().anyMatch(authority -> antPathMatcher.match(authority.getAuthority(), path));
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return false;
    }
    private SignedJWT getSignedJWT(String headerToken) throws ParseException {
        String token = headerToken.replace(JWTConstants.TOKEN_PREFIX, "");
        log.info("token is {}", token);
        return SignedJWT.parse(token);
    }
}

五、运行效果

我们现在数据库中添加几个菜单(这里为了简单直接在数据库添加在这里插入图片描述

添加角色: 在这里插入图片描述 给角色赋予权限 在这里插入图片描述

给用户赋予角色:

在这里插入图片描述 现在用admin的用户登录等到token: 在这里插入图片描述 用Token去访问order/list,看到这里返回order list, 权限认证成功。 在这里插入图片描述

如访问user/findByUsername这会提示无权限,这样就实现到具体URL的鉴权了。 在这里插入图片描述

六、权限变动重新授权

我们在JWTAuthenticationFilter中,把权限信息等写入到了redis中,key的规则如下

        //生成Key, 把权限放入到redis中
        String keyPrefix = "JWT" + user.getId() + ":";
        String keySuffix = Md5Utils.getMD5(jwtToken.getBytes());
        String key = keyPrefix + keySuffix;

        String authKey = key + ":Authorities";

redis中看到如下: 在这里插入图片描述 只要后台权限变动的时候,根据key的规则清除redis数据即可, 然后在gateway中获取不到相应的权限, 那么会要求用户重新登录。

到这里我们已经实现了第一节提的3个需求, 更多的玩法,由各位自己发挥了。

代码在这里哟 重要事情再说一遍

micro-service-skeleton's People

Contributors

yangxiufeng666 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

micro-service-skeleton's Issues

代码格式

代码格式太乱了,该对齐的没有对齐,看着太累

权限体系对比的是menu里面的code和request的URL地址?

您好,权限体系这块我看了您的实现方式,先获取用户role拥有的menu,再把menu的code放进去:
GrantedAuthority authority = new SimpleGrantedAuthority(menu.getCode());
grantedAuthorities.add(authority);
再跟requestURL对比,去判断是否有权限
antPathMatcher.match(authority.getAuthority(),requestUrl)
这个对比方式我不太理解工作的原理,是要求把menu和接口的地址写成一样的吗?

真诚请教一下,关于不同用户role角色是怎么玩的?

@OverRide
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
RcUserEntity userEntity = userService.findByUsername(username);
if (userEntity == null) {
throw new UsernameNotFoundException("用户:" + username + ",不存在!");
}
Set grantedAuthorities = new HashSet<>();
boolean enabled = true; // 可用性 :true:可用 false:不可用
boolean accountNonExpired = true; // 过期性 :true:没过期 false:过期
boolean credentialsNonExpired = true; // 有效性 :true:凭证有效 false:凭证无效
boolean accountNonLocked = true; // 锁定性 :true:未锁定 false:已锁定
List roleValues = roleService.getRoleValuesByUserId(userEntity.getId());
for (RcRoleEntity role:roleValues){
//角色必须是ROLE_开头,可以在数据库中设置
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+role.getValue());
grantedAuthorities.add(grantedAuthority);
//获取权限
List permissionList = permissionService.getPermissionsByRoleId(role.getId());
for (RcMenuEntity menu:permissionList
) {
GrantedAuthority authority = new SimpleGrantedAuthority(menu.getCode());
grantedAuthorities.add(authority);
}
}
User user = new User(userEntity.getUsername(), userEntity.getPassword(),
enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
return user;
}

这一块,请问作者具体是在哪里相关联的,假设我想设置授权给其它应用的访问权限跟自己网站不一样,请问该怎么配置呢?可以加个联系方式吗?

Bad credentials

我在下了源码之后,用postman在调试http://localhost:9030/uaa/oauth/token
  一直报下面的错误
{
"timestamp": 1539248009450,
"status": 401,
"error": "Unauthorized",
"message": "Bad credentials",
"path": "/oauth/token"
}
配置文件就是从这里git下来的,有人能帮我一下吗,详细说一下吗

走网关获取数据就是invalid_token, 这个是什么问题?

在Micro-Service-Skeleton-Gateway中有一段配置,如果是直接走网关user-info-uri: http://localhost:9030/uaa/user, 获取接口数据时显示的是invalid_token。

不走网关使用user-info-uri: http://localhost:9060/user 就是成功的,不知道有什么区别?
如果有多个Micro-Service-Skeleton-Auth服务,怎么实现高可用?难道要使用nginx进行代理吗?如果走网关应该是不需要的。

security:
oauth2:
resource:
#获得授权端的当前用户信息url
#user-info-uri: http://localhost:9030/uaa/user, 如果是这样就访问不了,不知道为啥子??
user-info-uri: http://localhost:9060/user
prefer-token-info: false

sql有错误

DROP TABLE IF EXISTS oauth_approvals;
CREATE TABLE oauth_approvals (
userId varchar(256) DEFAULT NULL,
clientId varchar(256) DEFAULT NULL,
scope varchar(256) DEFAULT NULL,
status varchar(10) DEFAULT NULL,
expiresAt timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
lastModifiedAt timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

[Err] 1067 - Invalid default value for 'lastModifiedAt'

sql导入报错!!!

token存储数据库

打开 Token 存入数据库
在启动项目的时候会报错

  • redisTokenStore: defined by method 'redisTokenStore' in class path resource [com/wealthquery/wealthqueryoauth/config/AuthorizationServerConfig.class]
    • jdbcTokenStore: defined by method 'jdbcTokenStore' in class path resource [com/wealthquery/wealthqueryoauth/config/AuthorizationServerConfig.class]

springcloud oauth2 集群配置

您好,如果我有多个autherver ,那么客户端应该怎么配置呢?难道cloud内部也要架个nginx么。。。

服务消费和安全

您好,我想问一下,资源服务被添加了安全限制之后,那么多个资源服务之间使用feign进行服务消费怎么办?会遇到401问题,这个怎么解决?

gateway 怎么拿到用户拥有的url的

认证服务器存的也只是 存的rolecode
Result<List> roleResult = roleService.getRoleByUserId(userVo.getId());
if (roleResult.getCode() != 100){
List roleVoList = roleResult.getData();
for (RoleVo role:roleVoList){
//角色必须是ROLE_开头,可以在数据库中设置
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_"+role.getValue());
grantedAuthorities.add(grantedAuthority);
//获取权限
Result<List> perResult = permissionService.getRolePermission(role.getId());
if (perResult.getCode() != 100){
List permissionList = perResult.getData();
for (MenuVo menu:permissionList
) {
GrantedAuthority authority = new SimpleGrantedAuthority(menu.getCode());
grantedAuthorities.add(authority);
}
}
而gateway
if (antPathMatcher.match(authority.getAuthority(),requestUrl))

只有一个APIGateway么

请问APIGateway层是只有一个对所有平台接入么,还是一个平台一个APIGateway,每个都不同,比如app应用和后台管理平台应用是对于两个不同的APIGateway,然后做不同的权限验证逻辑

monitor那个工程现在是不是不能用了,一是pom.xml注了zipkin的全部,二是MonitorApplication的注解@EnableZipkinServer出错!

关于上述问题,在fank 帮助下,已经解决了,主要原因就是这一次升级后数据库脚本发生很大变化,原来rc_开头的表,全部换成sys_开头了,另外,原来oauth_client_details 表 webApp那条记录密码由原来的webApp换成123456再加密的!在此感谢fank,同时在这里也提出下一个问题就是monitor那个工程现在是不是不能用了,一是pom.xml注了zipkin的全部,二是MonitorApplication的注解@EnableZipkinServer出错!
望各位同仁或已解决了朋友,提出指教一下!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.