微服务:gateway+security+nacos实现微服务的认证授权方案

1. 项目结构体

在这里插入图片描述
用户通过客户端访问项目时,前端项目会部署在nginx上,加载静态文件时直接从nginx上返回即可。
当用户在客户端操作时,需要调用后端的一些服务接口。这些接口会通过Gateway网关,网关进行一定的处理(jwt合法性校验,黑名单、白名单,过滤一部分请求)之后再转发给具体的微服务。
具体的资源服务会对请求进行解析,判断当前登录用户是否有权限调用该资源的接口。

2. 步骤

2.1. 统一认证服务

项目目录结构:
在这里插入图片描述

2.1.2. 引入依赖

下面两个为关键依赖,还可以自行补充nacos等依赖

	   <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-security</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-oauth2</artifactId>
       </dependency>

2.1.3. 引入代码

AuthorizationServer :

/**
 * @description 授权服务器配置  OAUTH2.0
 * @author Mr.M
 * @date 2022/9/26 22:25
 * @version 1.0
 */
 @Configuration
 @EnableAuthorizationServer
 public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
  @Resource(name="authorizationServerTokenServicesCustom")
  private AuthorizationServerTokenServices authorizationServerTokenServices;
 @Autowired
 private AuthenticationManager authenticationManager;
  //客户端详情服务
 /**
  * 用来配置客户端详情服务(ClientDetailsService),
  * 随便一个客户端都可以随便接入到它的认证服务吗?
  * 答案是否定的,服务提供商会给批准接入的客户端一个身份,用于接入时的凭据,
  * 有客户端标识和客户端秘钥,在这里配置批准接入的客户端的详细信息。
  */
  @Override
  public void configure(ClientDetailsServiceConfigurer clients)
          throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("XcWebApp")// client_id
//                .secret("XcWebApp")//客户端密钥
                .secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
                .resourceIds("xuecheng-plus")//资源列表
                .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //客户端接收授权码的重定向地址
                .redirectUris("http://ip:8100/api/user");
        /*
                1、get请求获取授权码
                地址: /oauth/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://ip:8100/api/user
                参数列表如下:
                •client_id:客户端准入标识。
                •response_type:授权码模式固定为code。
                •scope:客户端权限。
                •redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。

                2、请求成功,重定向至http://www.xuecheng-plus.com/?code=授权码,比如:http://www.xuecheng-plus.com/?code=Wqjb5H
                3、使用httpclient工具post申请令牌
                /oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=authorization_code&code=授权码&redirect_uri=http://ip:8100/api/user
                参数列表如下
                •client_id:客户端准入标识。
                •client_secret:客户端秘钥。
                •grant_type:授权类型,填写authorization_code,表示授权码模式
                •code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
                •redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

                4、申请令牌成功如下所示:
                JSON
                {
                  "access_token": "368b1ee7-a9ee-4e9a-aae6-0fcab243aad2",
                  "token_type": "bearer",
                  "refresh_token": "3d56e139-0ee6-4ace-8cbe-1311dfaa991f",
                  "expires_in": 7199,
                  "scope": "all"
                }
                说明:
                1、access_token,访问令牌,用于访问资源使用。
                2、token_type,bearer是在RFC6750中定义的一种token类型,在携带令牌访问资源时需要在head中加入bearer 空格 令牌内容
                3、refresh_token,当令牌快过期时使用刷新令牌可以再次生成令牌。
                4、expires_in:过期时间(秒)
                5、scope,令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权。
         */
  }

  //令牌端点的访问配置
 /**
  * 用来配置令牌(token)的访问端点和令牌服务(token services)。
  */
 @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
   endpoints
           .authenticationManager(authenticationManager)//认证管理器
           .tokenServices(authorizationServerTokenServices)//令牌管理服务
           .allowedTokenEndpointRequestMethods(HttpMethod.POST);
  }

  //令牌端点的安全配置
 /**
  * 用来配置令牌端点的安全约束.
  */
 @Override
  public void configure(AuthorizationServerSecurityConfigurer security){
   security
           .tokenKeyAccess("permitAll()")                    //oauth/token_key是公开
           .checkTokenAccess("permitAll()")                  //oauth/check_token公开
           .allowFormAuthenticationForClients()				//表单认证(申请令牌)
   ;
  }
 }

SecurityConfig :


@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //OAUTH2.0   配置认证管理bean
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
//    //配置用户信息服务  这里注释之后需要实现一个接口--UserDetailsService
//    @Bean
//    public UserDetailsService userDetailsService() {
//        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
//        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
//        return manager;
//    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        //密码为明文方式
//        return NoOpPasswordEncoder.getInstance();
        //密码加密
        return new BCryptPasswordEncoder();
    }
    //配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
                .anyRequest().permitAll()//其它请求全部放行
                .and()
                .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success
        http.logout().logoutUrl("/logout");//退出地址
    }
}

TokenConfig:

/**
 * 令牌策略配置类
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {
    private String SIGNING_KEY = "mq123";
    @Autowired
    TokenStore tokenStore;
    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
    //令牌管理服务
    @Bean(name="authorizationServerTokenServicesCustom")
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }
}

LoginController:

/**
 * @author Mr.M
 * @version 1.0
 * @description 测试controller
 * @date 2022/9/27 17:25
 */
@Slf4j
@RestController
public class LoginController {
    @Autowired
    private UserService userService;

    @RequestMapping("/login-success")
    public String loginSuccess(){
        return "登录成功";
    }

    @RequestMapping("/user/{id}")
    public Result<User> getuser(@PathVariable("id") Integer id){
        return userService.getUserByIdWithCache(id);
    }

    @RequestMapping("/r/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
    public String r1(){
        return "访问r1资源";
    }

    @RequestMapping("/r/r2")
    @PreAuthorize("hasAuthority('p2')")//拥有p1权限方可访问
    public String r2(){
        return "访问r2资源";
    }
}

LoginUserServiceImpl :

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/28 18:09
 */
@Service
@Slf4j
public class LoginUserServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    /**
     * @description 根据账号查询用户信息
     * @param s  账号
     * @return org.springframework.security.core.userdetails.UserDetails
     * @author Mr.M
     * @date 2022/9/28 18:30
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        final QueryWrapper<User> objectQueryWrapper = new QueryWrapper<>();
        objectQueryWrapper.eq("username",s);
        final User user = userMapper.selectOne(objectQueryWrapper);

        if(user==null){
            //返回空表示用户不存在
            return null;
        }
        log.info("UserServiceImpl:{}" ,user.toString());

        //取出数据库存储的正确密码
        String password  =user.getPassword();
        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities= {"p1"};
        //为了安全在令牌中不放密码
        user.setPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);

        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = org.springframework.security.core.userdetails.User
                .withUsername(userString).password(password).authorities(authorities).build();
        return userDetails;
    }

    public static void main(String[] args) {
        String password = "1234";
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        for(int i=0;i<10;i++) {
            //每个计算出的Hash值都不一样
            String hashPass = passwordEncoder.encode(password);
            System.out.println(hashPass);
            //虽然每次计算的密码Hash值不一样但是校验是通过的
            boolean f = passwordEncoder.matches(password, hashPass);
            System.out.println(f);
        }
    }

}

LoginUserServiceImpl 实现了 UserDetailsService 之后,在security登录的时候,就会根据LoginUserServiceImpl 中的loadUserByUsername对登录的用户的账号密码进行校验。
然后将用户信息加入org.springframework.security.core.userdetails.User中,由于上述代码的配置,使用的是jwt的配置。
使用密码模式,可以获取到jwt的token信息如下图所示:
http://localhost:8101/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=zhangsan&password=123
在这里插入图片描述
access_token中,就会包含用户的信息。(注意token中不要存放密码等敏感信息)

2.2. 其他服务

由于其他服务也需要收到security的保护,所以需要进行一定的配置。
在这里插入图片描述

2.2.1. 引入依赖

        <!--认证相关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <!-- nacos客户端依赖包 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <!--&lt;!&ndash;       nacos的配置管理依赖 &ndash;&gt;-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

2.2.2. 引入代码

ResouceServerConfig :

/**
 * 资源服务配置
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {

    //资源服务标识,需要和认证服务的标识符保持一致
    public static final String RESOURCE_ID = "xuecheng-plus";

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/**","/course/**","/api/**").authenticated()//所有/api/**的请求必须认证通过
                //根据配置可知/course/**开头的接口需要认证通过。
                .anyRequest().permitAll()
        ;
    }
}

TokenConfig :


/**
 * 令牌策略配置类
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    //jwt签名密钥,与认证服务保持一致,需要相同的密钥才能成功解析token
    private String SIGNING_KEY = "mq123";

    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

   /* @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }*/
}

配置完成之后,请求该服务的接口时,需要带上token才能访问。
在这里插入图片描述
在这里插 片描述

2.3. 网关服务

在这里插入图片描述

2.3.1. 引入依赖

<!-- nacos客户端依赖包 -->

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>
        <!--&lt;!&ndash;       nacos的配置管理依赖 &ndash;&gt;-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

2.3.2. 引入代码

GatewayAuthFilter :

@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
    //白名单
    private static List<String> whitelist = null;

    static {
        //加载白名单
        try (
                InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
        ) {
            Properties properties = new Properties();
            properties.load(resourceAsStream);
            Set<String> strings = properties.stringPropertyNames();
            System.out.println(strings.toString());
            whitelist= new ArrayList<>(strings);

        } catch (Exception e) {
            log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
            e.printStackTrace();
        }
    }
    @Autowired
    private TokenStore tokenStore;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestUrl = exchange.getRequest().getPath().value();
        AntPathMatcher pathMatcher = new AntPathMatcher();//用于做路径判断
        //白名单放行   白名单不需要jwttoken
        for (String url : whitelist) {
            if (pathMatcher.match(url, requestUrl)) {
                return chain.filter(exchange);
            }
        }

        //检查token是否存在
        String token = getToken(exchange);
        if (!StringUtils.isNotEmpty(token)) {
            return buildReturnMono("没有认证",exchange);
        }
        //判断是否是有效的token
        OAuth2AccessToken oAuth2AccessToken;
        try {
            oAuth2AccessToken = tokenStore.readAccessToken(token);

            boolean expired = oAuth2AccessToken.isExpired();
            if (expired) {
                return buildReturnMono("认证令牌已过期",exchange);
            }
            return chain.filter(exchange);
        } catch (InvalidTokenException e) {
            log.info("认证令牌无效: {}", token);
            return buildReturnMono("认证令牌无效",exchange);
        }
    }

    /**
     * 获取token
     */
    private String getToken(ServerWebExchange exchange) {
        String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (!StringUtils.isNotEmpty(tokenStr)) {
            return null;
        }
        String token = tokenStr.split(" ")[1];
        if (!StringUtils.isNotEmpty(token)) {
            return null;
        }
        return token;
    }

    //返回响应结果
    private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        String jsonString = JSON.toJSONString(new RestErrorResponse(error));
        byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }

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

RestErrorResponse :

/**
 * 错误响应参数包装
 */
public class RestErrorResponse implements Serializable {

    private String errMessage;

    public RestErrorResponse(String errMessage){
        this.errMessage= errMessage;
    }

    public String getErrMessage() {
        return errMessage;
    }

    public void setErrMessage(String errMessage) {
        this.errMessage = errMessage;
    }
}

SecurityConfig :

/**
 * @description 安全配置类
 * @author Mr.M
 * @date 2022/9/27 12:07
 * @version 1.0
 */
 @EnableWebFluxSecurity
 @Configuration
 public class SecurityConfig {


  //安全拦截配置
  @Bean
  public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {

   return http.authorizeExchange()
           .pathMatchers("/**").permitAll()
           .anyExchange().authenticated()
           .and().csrf().disable().build();
  }


 }

TokenConfig :

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    String SIGNING_KEY = "mq123";

//    @Bean
//    public TokenStore tokenStore() {
//        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
//    }

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }


}

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值