欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

程序员文章站 2022-03-14 10:21:54
...

OAuth2 分布式认证授权

整体流程

架构方案图:

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

流程:

1、UAA 认证服务负责认证授权。

2、所有请求经过网关到达微服务。

3、网关负责鉴权客户端以及请求转发。

4、网关将 token 解析后传给微服务,微服务进行授权。

整个演示项目包含4个模块:注册中心、认证服务、资源服务、网关服务。

注册中心

我们使用前面创建的 eureka-registry-center 作为注册中心,修改 授权服务 与 资源服务 分别添加注册中心相关配置。具体修改如下:

一、引入 pom 依赖。【授权服务与资源服务都要】

<!-- 引入 eureka client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

二、添加 yml 配置。注意除了服务实例 Id 外其它配置一样,授权服务实例 Id 为 security-oauth2-auth3030,资源服务实例 Id 为 security-oauth2-resource3040

eureka-connection:
  name: akieay
  password: 1qaz2wsx
eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      #      defaultZone: http://localhost:7001/eureka
      defaultZone: http://${eureka-connection.name}:${eureka-connection.password}@eureka7001.com:7001/eureka,http://${eureka-connection.name}:${eureka-connection.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: 服务实例Id
    ## 当调用getHostname获取实例的hostname时,返回ip而不是host名
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
    lease-renewal-interval-in-seconds: 10
    # Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
    lease-expiration-duration-in-seconds: 30

分别启动注册中心集群 、授权服务、资源服务;进入注册中心查看服务注册情况。如下图:
Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

网关

网关整合 OAuth2.0 有两种思路,一种是认证服务器生成 JWT 令牌,所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发。

我们选用第一种。把 API 网关作为 OAuth2.0 的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息(jsonToken)给微服务。这样做的好处就是:下游微服务不需要关心令牌格式解析以及 OAuth2.0 相关机制,专注于自身服务。

API 网关在认证授权体系里主要负责两件事:

  • 作为 OAuth2.0 的资源服务器角色,实现第三方权限拦截。
  • 令牌解析并转发当前登录用户信息(明文 token)给微服务。

微服务拿到明文 token(明文 token 中包含登录用户的身份和权限信息)后也需要做两件事:

  • 用户授权拦截(查看当前用户是否有权访问该资源)
  • 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)
准备工作

为了方便配置网关服务,我们为 授权服务 与 资源服务 添加 context-path 配置,其中以 /auth 为访问路径前缀的是认证服务,以 /api 为前缀的是资源服务。具体配置如下:

server:
  port: 3030
  servlet:
    context-path: /auth
server:
  port: 3040
  servlet:
    context-path: /api
使用 Zuul 网关模块

我们使用前面创建的 routing-zuul-service 做为服务网关。
一、修改 yml 配置,具体配置如下:

server:
  port: 6070

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: routing-zuul-servie

zuul:
  host:
    connect-timeout-millis: 15000 #HTTP连接超时要比Hystrix的大
    socket-timeout-millis: 60000   #socket超时
  retryable: true
  add-proxy-headers: true
  #关闭默认路由配置
  ignored-services: '*'
  #配置过滤敏感的请求头信息,默认不会过滤
  sensitive-headers: '*'
  add-host-header: true #设置为true重定向是会添加host请求头
  routes:
    SECURITY-OAUTH2-AUTH-SERVICE:
      stripPrefix: false
      path: /auth/**
    SECURITY-OAUTH2-RESOURCES-SERVICE:
      stripPrefix: false
      path: /api/**

ribbon:
  ReadTimeout: 10000
  ConnectTimeout: 10000

feign:
  hystrix:
    enabled: true
  compression:
    request:
      enabled: true
      mime-types:
        - text/xml
        - application/xml
        - application/json
      min-request-size: 2048
    response:
      enabled: true

eureka-connection:
  name: akieay
  password: 1qaz2wsx
eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      #      defaultZone: http://localhost:7001/eureka
      defaultZone: http://${eureka-connection.name}:${eureka-connection.password}@eureka7001.com:7001/eureka,http://${eureka-connection.name}:${eureka-connection.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: routing-zuul-6070
    ## 当调用getHostname获取实例的hostname时,返回ip而不是host名
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
    lease-renewal-interval-in-seconds: 10
    # Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
    lease-expiration-duration-in-seconds: 30

logging:
  level:
    root: info
    com.akieay.cloud.zuul: debug

二、添加 token 配置。

前面也介绍了,资源服务器由于需要验证并解析令牌,往往可以通过在授权服务器暴露 check_tokenEndpoint 来完成,而我们在授权服务器使用的是对称加密的 jwt ,因此知道**即可,资源服务与授权服务本就是对称设计,那我们把授权服务的 TokenConfig 这个类拷贝过来就行。

@Configuration
public class TokenConfig {

    /**
     * 签名秘钥
     */
    private static String SIGNING_KEY = "akieay-security-oauth2-signing-key";

    /**
     * 令牌存储策略
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    /**
     * JWT访问令牌转换器
     *
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        // 对称秘钥,资源服务器使用该**来验证
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }

}

三、网关资源服务配置。

ResouceServerConfig 中定义资源服务配置,主要配置的内容就是定义一些匹配规则,描述某个接入客户端需要什么样的权限才能访问某个微服务,如下:配置了 authapi 服务的权限校验规则。

@Configuration
public class ResourceServerConfig {

    public static final String RESOURCE_ID = "res1";

    /**
     * auth 授权服务配置
     *
     */
    @Configuration
    @EnableResourceServer
    public class AuthServerConfig extends ResourceServerConfigurerAdapter {
        @Resource
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            // 资源ID
            resources.resourceId(RESOURCE_ID)
                    // 本地校验令牌
                    .tokenStore(tokenStore)
                    //不把token信息记录在session中
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/auth/**").permitAll();
        }
    }

    /**
     * api 资源服务配置
     */
    @Configuration
    @EnableResourceServer
    public class ApiServerConfig extends ResourceServerConfigurerAdapter {
        @Resource
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            // 资源ID
            resources.resourceId(RESOURCE_ID)
                    // 本地校验令牌
                    .tokenStore(tokenStore)
                    //不把token信息记录在session中
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/api/**").access("#oauth2.hasScope('all')");
        }
    }

}

上面定义了两个微服务的资源,其中:

  • AuthServerConfig 配置指定:若请求匹配 /auth/** 网关不进行拦截。
  • ApiServerConfig 配置指定:若请求匹配 /api/** ,则要求接入客户端需要在 scope 中包含 all 【即要求存在权限范围 all】。

四、安全配置。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 授权拦截机制
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().authorizeRequests()
                .antMatchers("/**").permitAll();
    }

}

五、转发明文 token 给微服务。

通过 Zuul 过滤器的方式实现,目的是让下游的微服务能够很方便的获取到当前的登录用户信息(明文 token)。

(1)实现 Zuul 前置过滤器,完成当前登录用户信息提取,并放入转发微服务的 request 中。

public class AuthFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

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

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        // 1.获取令牌内容
        RequestContext ctx = RequestContext.getCurrentContext();
        // 从安全上下文中获取 用户身份对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!(authentication instanceof OAuth2Authentication)) {
            // 无 token 访问网关内资源的情况,目前仅有 auth 服务直接暴露
            return null;
        }

        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();

        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        // 2.组装明文 token,转发给微服务,放入 header,名称为 json-token
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String, Object> jsonToken = new HashMap<>(requestParameters);
        if (null != userAuthentication) {
            // 取出用户身份信息
            String name = userAuthentication.getName();
            List<String> authorities = new ArrayList<>();
            // 获取用户权限信息
            userAuthentication.getAuthorities().stream().forEach(c -> authorities.add(c.getAuthority()));

            jsonToken.put("principal", name);
            jsonToken.put("authorities", authorities);
        }
        // 把身份信息和权限信息放在 json 中,加入 http 的 header 中
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSONUtil.toJsonStr(jsonToken)));
        return null;
    }
}

(2)将 filter 添加到 Spring 容器

@Configuration
public class ZuulConfig {

    @Bean
    public AuthFilter preFilter() {
        return new AuthFilter();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

微服务解析令牌并鉴权

修改资源服务,当微服务收到明文 token 时,我们可以通过继承 OncePerRequestFilter 实现对微服务的用户鉴权拦截功能。

用户身份信息实体类

@Data
@EqualsAndHashCode(callSuper = false)
public class AccountInfo {

    private Integer id;
    /**
     * 账号
     */
    private String accountName;
    /**
     * 昵称
     */
    private String nickName;
    /**
     * 盐值
     */
    private String salt;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 联系电话
     */
    private String phone;
    /**
     * 状态 ('N': 正常, 'F': 禁用, 'D': 删除)
     */
    private String status;
    /**
     * 创建时间
     */
    private Date gmtCreate;
    /**
     * 创建者
     */
    private Integer createAccountId;
    /**
     * 修改时间
     */
    private Date gmtModified;
    /**
     * 修改者
     */
    private Integer modifiedAccountId;
}

令牌解析过滤器

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取 token
        String token = request.getHeader("json-token");
        if (null != token) {
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            // 将 token 转成 json 对象
            JSONObject jsonObject = JSONUtil.parseObj(json);
            // 用户身份信息
            String principal = jsonObject.getStr("principal");
            AccountInfo accountInfo = new AccountInfo();
            accountInfo.setAccountName(principal);

            // 用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.toArray().length]);

            // 将用户信息和权限填充到用户身份 token 对象中
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo,
                    null, AuthorityUtils.createAuthorityList(authorities));
            // 将 authenticationToken 填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        filterChain.doFilter(request, response);
    }
}

以上的令牌解析过滤器,解析从 Zuul 网关服务转发过来的 token,并获取其中的用户身份信息 和 用户权限填充到 Security 的安全上下文中,以实现对用户访问的鉴权。

资源服务添加 测试方法

@RestController
@RequestMapping("/v1/test")
public class TestController {

    @GetMapping("/getAuthorInfo")
    @PreAuthorize("hasAuthority('r:r3')")
    public Map<String, Object> getAuthorInfo() {
        Map<String, Object> result = new HashMap<>(6);
        result.put("author", "akieay");
        result.put("date", "2020-11-30");
        result.put("method", "getAuthorInfo");
        return result;
    }

    @GetMapping("/getJwtTokenInfo")
    @PreAuthorize("hasAuthority('r:r1')")
    public JSON getJwtTokenInfo(Authentication authentication, HttpServletRequest request) {
        AccountInfo accountInfo = (AccountInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return JSONUtil.parse(accountInfo);
    }

}

集成测试

分别启动 eureka 注册中心集群节点、认证服务auth、资源服务api、网关服务zuul,并打开 eureka 注册中心查看服务注册情况。

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

使用 密码模式 获取令牌【通过网关】,访问:http://localhost:6070/auth/oauth/token

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

验证令牌,访问:http://localhost:6070/auth/oauth/check_token

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

访问资源服务 getJwtTokenInfogetAuthorInfo;由于 zhangsan 具有 r:r1、r:r2、r:r3 权限,所以这两个接口都能访问;而由于 lisi 只具有 r:r1、r:r2 没有 r:r3 权限,所以只能访问 getJwtTokenInfo 没有 getAuthorInfo 的访问权限。如下:

张三访问:
Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

李四访问:

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

扩展用户信息

目前 JWT 令牌存储了用户的身份信息、权限信息,网关将 token 明文转发给微服务使用,目前用户身份信息仅包含了用户的账号;在实际开发中可能还需要用户的其它信息,所以我们需要扩展用户信息。

思路:在认证阶段 DaoAuthenticationProvider 会调用 UserDetailService 查询用户的信息,这里是可以获取到齐全的用户信息的。由于 JWT 令牌中用户身份信息来源于 UserDetailsUserDetails 中仅定义了 username 为用户的身份信息,所以这里提供了两个扩展用户信息的思路:

  • 第一是可以扩展 UserDetails ,使之包含更多的自定义属性。
  • 第二也可以扩展 username 的内容,比如存入用户信息的 json 字符串内容作为 username 的内容。

相比较而言,方案二比较简单还不用破坏 UserDetails 的结构,所以我们采用方案二。具体修改如下:

一、修改 认证服务的 CustomUserDetailsService,具体修改如下:

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysAccountEntity sysAccount = sysAccountMapper.getByAccountName(username);
    if (null == sysAccount) {
        return null;
    }

    List<String> permissions = sysAccountMapper.listPermissionByAccountId(sysAccount.getId());
    String[] authorities = permissions.toArray(new String[permissions.toArray().length]);

    String password = sysAccount.getPassword();
    sysAccount.setPassword("");
    String accountJson = JSONUtil.toJsonStr(sysAccount);
    UserDetails userDetails = User.withUsername(accountJson).password(password)
        .authorities(AuthorityUtils.createAuthorityList(authorities)).build();
    return userDetails;
}

二、修改 资源服务的 TokenAuthenticationFilter,具体修改如下:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 获取 token
    String token = request.getHeader("json-token");
    if (null != token) {
        String json = EncryptUtil.decodeUTF8StringBase64(token);
        // 将 token 转成 json 对象
        JSONObject jsonObject = JSONUtil.parseObj(json);
        // 用户身份信息
        String principal = jsonObject.getStr("principal");
        AccountInfo accountInfo = JSONUtil.parseObj(principal).toBean(AccountInfo.class);

        // 用户权限
        JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
        String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.toArray().length]);

        // 将用户信息和权限填充到用户身份 token 对象中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountInfo,
                                                                                                          null, AuthorityUtils.createAuthorityList(authorities));
        // 将 authenticationToken 填充到安全上下文
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
    filterChain.doFilter(request, response);
}

三、重启服务,重新获取令牌,并访问资源服务的 getJwtTokenInfo 接口。这时我们就可以获取到完整的用户信息了。

Spring Cloud 入门 ---- OAuth2 分布式认证授权【随笔】

相关标签: spring cloud