SpringCloud(六) 微服务安全实战 JWT认证

前后端分离的微服务间安全通讯:

之前的认证方式存在三个问题:

    1.效率低,每次认证都需要去认证服务器去调用认证服务

    2.不安全,传递用户身份是通过在请求头中通过明文传递

    3.服务间传递信息比较麻烦,需要在请求头中存储用户信息才能处理

以上问题解决方案就需要使用JWT。

JWT(Java Web Token):

    

code:

    在auth服务器中修改

/**
 * @author aric
 * @create 2021-04-06-10:54
 * @fun 认证服务器配置
 */
@Configuration
@EnableAuthorizationServer  //作为授权服务器存在注解
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private PasswordEncoder passwordEncoder;

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

    @Bean
    public JwtAccessTokenConverter jwtTokenEnhancer() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        //converter.setSigningKey("123456"); 对数据进行签名,注意不要泄漏,不然会别人签的也会认,一般会用证书进行签名,用一下替换
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("xuyu.key") /*证书的名字*/, "123456".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("xuyu")); //作为加密的私钥
        return converter;
    }


    //让认证服务器知道哪些应用回来找它要令牌
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //先注册到内存,后面到数据库操作
        clients.inMemory()
                .withClient("orderApp")
                .secret(passwordEncoder.encode("123456"))  //应用的名字和密码
                .scopes("read","write")  //访问资源服务器 -> 做权限控制
                .accessTokenValiditySeconds(3600)  //令牌有效期
                .resourceIds("order-server")  //可以访问的资源服务器
                .authorizedGrantTypes("password")  //授权方式
    }

    //让认证服务器知道哪些人回来访问
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints){
        endpoints
                .tokenStore(tokenStore())
                .tokenEnhancer(jwtTokenEnhancer())
                .authenticationManager(authenticationManager);  //托管给authenticationManager做校验用户信息是否合法
    }

    //订单服务(谁)找认证服务器验证
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security){
        security
                .tokenKeyAccess("isAuthenticated()")  //oAuth往外暴露一个服务,只有经过认证的请求才能拿到tokenKey,去验证签名
                .checkTokenAccess("isAuthenticated()");
    }
}

    修改网关相关配置:

pom.xml添加:

#配置获取验证jwt签名的key的地址,就可以把auth服务器中的密钥返回回来
security:
  oauth2:
#    访问这个url必须身份认证
    client:
      client-id: gateway
      client-sercret: 123465
#    获取密钥的地址
    resource:
      jwt:
        key-uri: http://auth.xuyu.com:9090/oauth/oauth/token_key

配置类直接用一下替换:
/**
 * @author aric
 * @create 2021-05-24-10:46
 * @fun
 */
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter{

    /*配置表达式处理器*/
    @Autowired
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources){
        resources.expressionHandler(gatewayWebSecurityExpressionHandler);
    }

    public void configure(HttpSecurity http){
        http.authorizeRequests()
            .antMatchers("/token/**").permitAll()  //申请令牌的请求不需要携带令牌
            // .anyRequest().authenticated(); //默认禁用所有请求都需要身份认证
            .anyRequest().access("#permissionService.hasPermission(request,authentication)")  //指定访问规则
    }
}

使用表达式处理器
/**
 * @author aric
 * @create 2021-05-24-14:20
 * @fun
 */
@Service
public class PermissionServiceImpl implements PermissionService {
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        System.out.println(request.getRequestURI());
        System.out.println(ReflectionToStringBuilder.toString(authentication));
        //有50%几率有权限访问服务
        return RandomUtils.nextInt() % 2 == 0;
    }
}

表达式处理器实现:
/**
 * @author aric
 * @create 2021-05-24-14:24
 * @fun 表达式处理器
 */
@Component
public class GatewayWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler {

    @Autowired
    private PermissionServiceImpl permissionService;

    @Override
    protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication,FilterInvocation invocation){
        StandardEvaluationContext sec = super.createEvaluationContextInternal(authentication,invocation);
        sec.setVariable("permissionService",permissionService);
        return sec;
    }
}

    订单服务修改:

pom.xml添加:

#配置获取验证jwt签名的key的地址,就可以把auth服务器中的密钥返回回来
security:
  oauth2:
    #    访问这个url必须身份认证
    client:
      client-id: orderService
      client-sercret: 123465
    #    获取密钥的地址
    resource:
      jwt:
        key-uri: http://auth.xuyu.com:9090/oauth/oauth/token_key

启动类添加注解:
@Configuration
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableResourceServer
@EnableGloabalMethodSecurity(prePostEnable = true)
public class OrderApi {

    @Bean
    public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context){
        return new OAuth2RestTemplate(resource, context);
    }

    public static void main(String[] args) {
        SpringApplication.run(OrderApi.class,args);
    }
}

UserDetailsSerImpl配置类:
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        user.setUsername(username);
        user.setId(1L);
        return user;
    }

    private PasswordEncoder passwordEncoder;

    public UserDetails loadUserByUsername2(String username){
        return User.withUsername(username)
                .password(passwordEncoder.encode("123456"))
                .authorities("ROLE_ADMIN")
                .build();
    }
}


登录接口:
@RestController
@RequestMapping("/orders")
@Slf4j
public class OrderController {

    @Autowired
    private OAuth2RestTemplate oauthRestTemplate;

    //远程调用

    @PostMapping
    //ACL方式根据oauth的Scope权限认证,须在启动类上添加@EnableGloabalMethodSecurity(prePostEnable = true)
    @PreAuthorize("#oauth2.hasScope('write')")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public OrderInfo create(@RequestBody OrderInfo info){
        PriceInfo price = oauthRestTemplate.getForObject("http://localhost:9060/prices/" + info.getProductId(), PriceInfo.class);
        log.info("price is "+ price.getPrice());
        return info;
    }

    //带有jwt令牌的的处理逻辑
    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info, @AuthenticationPrincipal String username){
        log.info("user is "+ username);
        return info;
    }
}

订单服务调用价格服务:

配置文件:
server:
  port: 9060

security:
  oauth2:
    client:
      client-id: priceService
      client-secret: 123456
    resource:
      jwt:
        key-uri: http://auth.xuyu.com:9090/oauth/token_key

启动类添加:
@EnableResourceServer

Controller:
@RestController
@RequestMapping("/prices")
@Slf4j
public class PriceController {

    @GetMapping("/{id}")
    public PriceInfo getPrice(@PathVariable Long id, @AuthenticationPrincipal String username){
        System.out.println("productId is"+ id);
        System.out.println("user is"+ username);
    }
}

最后微服务模型:

网关添加日志记录:

日志过滤:
public class GatewayAuditLogFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String user = (String)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println("1. add log for" +user);

        filterChain.doFilter(request,response);

        if(StringUtils.isNotBlank((String) request.getAttribute("logUpdated"))) {
            System.out.println("3. update log to success");
        }
    }
}

配置信息加载:
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter{

    /*配置表达式处理器*/
    @Autowired
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    /*自己实现的错误处理方式*/
    @Autowired
    private GatewayAccessDeniedHandler accessDeniedHandler;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources){
        resources
                .accessDeniedHandler(accessDeniedHandler)  //处理异常信息
                .expressionHandler(gatewayWebSecurityExpressionHandler);
    }

    @Override
    public void configure(HttpSecurity http){
        http.authorizeRequests()
                .addFilterBefore(new GatewayAuditLogFilter(),ExceptionTranslationFilter.class)
                .antMatchers("/token/**").permitAll()  //申请令牌的请求不需要携带令牌
//                .anyRequest().authenticated(); //默认禁用所有请求都需要身份认证
                .anyRequest().access("#permissionService.hasPermission(request,authentication)");  //使用给自己配置的表达式处理器
    }
}


日志实现:
@Component
public class GatewayAccessDeniedHandler extends OAuth2AccessDeniedHandler{

    @Override
    public void handle(HttpServlet request, HttpServletResponse response, AccessDeniedException authException){
        System.out.println("2. update log 403");
        request.setAttribute("logUpdated","yes");
        super.handle(request,response,authException);
    }
}

认证错误处理:
@Component
public class GatewayAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException){

        if(authException instanceof AccessToeknRequiredException){
            //没有传入令牌
            System.out.println("2. update log to 401");
        } else {
            //令牌有问题
            System.out.println("2. add log 401");
        }

        request.setAttribute("logUpdated","yes");
        super.commence(request, response, authException);
    }
}

权限没有认证错误:
@Service
public class PermissionServiceImpl implements PermissionService {
    @Override
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        System.out.println(request.getRequestURI());
        System.out.println(ReflectionToStringBuilder.toString(authentication));
        if(authentication instanceof AnonymousAuthenticationToken){
            throw new AccessTokenRequiredException(null);
        }
        //有50%几率有权限访问服务
        return RandomUtils.nextInt() % 2 == 0;
    }
}

网关添加限流:

/**
 * @author aric
 * @create 2021-05-24-17:06
 * @fun 限流过滤器
 */
public class GatewayRateLimitFilter extends OncePerRequestFilter {

    private RateLimiter rateLimiter = RateLimiter.create(1);


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        System.out.println("0 rate limit");
        if(rateLimiter.tryAcquire()){
            //获取到权限
            filterChain.doFilter(request,response);
        }else{
            //获取不到权限
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.setContentType("application/json");
            response.getWriter().write("{\"error\":\"too many request\"}");
            response.getWriter().flush();
            return;
        }
    }
}

将限流加入Config:
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter{

    /*配置表达式处理器*/
    @Autowired
    private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;

    /*自己实现的错误处理方式*/
    @Autowired
    private GatewayAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private GatewayAuthenticationEntryPoint authenticationEntryPoint;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources){
        resources
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler)  //处理异常信息
                .expressionHandler(gatewayWebSecurityExpressionHandler);
    }

    //过滤器链
    @Override
    public void configure(HttpSecurity http){
        http
            .addFilterBefore(new GatewayRateLimitFilter(),SecurityContextPersistenceFilter.class) //限流
            .addFilterBefore(new GatewayAuditLogFilter(),ExceptionTranslationFilter.class)  //日志
            .authorizeRequests()
            .antMatchers("/token/**").permitAll()  //申请令牌的请求不需要携带令牌
//          .anyRequest().authenticated(); //默认禁用所有请求都需要身份认证
            .anyRequest().access("#permissionService.hasPermission(request,authentication)");  //使用给自己配置的表达式处理器
    }
}

总结:

    jwt改造后网关所经历的顺序:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值