06从零开始学习微服务之保护微服务(认证、授权及保护资源)

1 OAuth2

OAuth2是一个基于令牌的安全验证和授权框架,它将安全性分解为以下4 个组成部分。

  1. 受保护资源:这是开发人员想要保护的资源, 需要确保只有已通过验证并且具有适当授权的用户才能访问它。
  2. 资源所有者:资源所有者定义哪些应用程序可以调用其服务,哪些用户可以访问该服务,以及他们可以使用该服务完成哪些事情。资源所有者注册的每个应用程序都将获得一个应用程序名称,该应用程序名称与应用程序密钥一起标识应用程序。应用程序名称和密钥的组合是在验证OAuth2令牌时传递的凭据的一部分。
  3. 应用程序:这是代表用户调用服务的应用程序。毕竟,用户很少直接调用服务。相反,他们依赖应用程序为他们工作。
  4. OAuth2 验证服务器:OAuth2验证服务器是应用程序和正在使用的服务之间的中间人。OAuth2验证服务器允许用户对自己进行验证,而不必将用户凭据传递给由应用程序代表用户调用的每个服务。

这4个组成部分互相作用对用户进行验证。用户只需提交他们的凭据。如果他们成功通过验证,则会出示一个验证令牌,该令牌可在服务之间传递,如下图所示。

资源保护主要分三步:

(1)构建验证服务,这个服务负责验证客户端身份并给客户端颁发令牌

(2)配置资源保护,只有携带令牌的客户端可以访问资源,其他的被阻挡

(3)令牌应可以在服务间传播

2 构建验证服务

2.1 Maven 构建依赖项

第一个依赖项spring-cloud-security引入了通用Spring 和Spring Cloud 安全库。第二个依赖项spring-security-oauth2拉取了Spring 0Auth2库。

   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
   </dependency>

2.2 创建AuthorizationServerConfig类

这个类扩展了Spring的AuthenticationServerConfigurer类,它提供了执行关键验证和授权功能的基本机制。下面分别介绍该类中核心的方法。

方法1:public void configure(ClientDetailsServiceConfigurer clients),客户端服务端点配置,即哪些客户端被注册到了服务。ClientDetailsServiceConfigurer类支持两种不同类型的存储:内存存储和JDBC存储。对本例来说,我们将使用JDBC存储。首先,查看ClientDetailsServiceConfigurer的源码,查看源码才知道我们要建立哪些数据库表来存储客户端信息。表的名字不能乱取。

从数据库查看哪些客户端注册到了认证服务。

   public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(this.dataSource).clients(this.clientDetails());
    }

方法2:public void configure(AuthorizationServerEndpointsConfigurer endpoints),授权端点配置,即配置认证管理器以及用户信息。有了用户信息就可以查看用户拥有的权限。

   //授权服务器端点配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                .authenticationManager(authenticationManager)//认证管理器
                .userDetailsService(userDetailsService);//用户信息service
    }

现在肯定有疑问了,认证管理的Bean和用户信息的Bean从哪里来的啊,下面我们就介绍其来源。

2.3 创建WebSecurityConfig类

要创建用户(及其角色),要从扩展WebSecurityConfigurerAdapter 类。SpringSecurity的实现方式类似于将乐高积木搭在一起来制造玩具车或模型。因此,我们需要为0Auth2服务器提供一种验证用户的机制, 并返回正在验证的用户的用户信息。这通过在Spring WebSecurityConfigurerAdapter 实现中定义authenticationManagerBean()和userDetailsServiceBean()两个bean 来完成。

注意不要混淆术语验证(authentication)和授权(authorization)的含义。验证是用户通过提供凭据来证明他们是谁的行为。授权决定是否允许用户做他们想做的事情。例如, Jim可以通过提供用户ID和密码来证明他的身份,但是他可能没有被授权查看敏感数据,如工资单数据。

方法1:authenticationManagerBean(),返回认证管理Bean。使用父类WebSecurityConfigurerAdapter中的默认验证。

 @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

方法2:userDetailsServiceBean(),我们没有使用父类WebSecurityConfigurerAdapter中的默认的。我们定义一个继承自UserDetailsService的类。

1 客户端要经过验证,如果通过验证,就没必要再返回用户信息。

2 通过验证的客户端,我们把存储的用户信息返回过去。

package com.xuecheng.auth.service;

import com.xuecheng.framework.domain.ucenter.XcMenu;
import com.xuecheng.framework.domain.ucenter.ext.XcUserExt;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    ClientDetailsService clientDetailsService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //取出身份,如果身份为空说明没有认证
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
        if(authentication==null){
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if(clientDetails!=null){
                //密码
                String clientSecret = clientDetails.getClientSecret();
                return new User(username,clientSecret,AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }
        if (StringUtils.isEmpty(username)) {
            return null;
        }
        XcUserExt userext = new XcUserExt();
        userext.setUsername("itcast");
        userext.setPassword(new BCryptPasswordEncoder().encode("123"));
        userext.setPermissions(new ArrayList<XcMenu>());
        if(userext == null){
            return null;
        }
        //取出正确密码(hash值)
        String password = userext.getPassword();
        //这里暂时使用静态密码
//       String password ="123";
        //用户权限,这里暂时使用静态数据,最终会从数据库读取
        //从数据库获取权限
        List<XcMenu> permissions = userext.getPermissions();
        List<String> user_permission = new ArrayList<>();
        permissions.forEach(item-> user_permission.add(item.getCode()));
//        user_permission.add("course_get_baseinfo");
//        user_permission.add("course_find_pic");
        String user_permission_string  = StringUtils.join(user_permission.toArray(), ",");
        UserJwt userDetails = new UserJwt(username,
                password,
                AuthorityUtils.commaSeparatedStringToAuthorityList(user_permission_string));
        userDetails.setId(userext.getId());
        userDetails.setUtype(userext.getUtype());//用户类型
        userDetails.setCompanyId(userext.getCompanyId());//所属企业
        userDetails.setName(userext.getName());//用户名称
        userDetails.setUserpic(userext.getUserpic());//用户头像
       /* UserDetails userDetails = new org.springframework.security.core.userdetails.User(username,
                password,
                AuthorityUtils.commaSeparatedStringToAuthorityList(""));*/
//                AuthorityUtils.createAuthorityList("course_get_baseinfo","course_get_list"));
        return userDetails;
    }
}

2.4 根据用户名和密码生成token

1 验证客户端

2 根据用户输入的信息,查看用户权限。

3 获取token

刷新令牌:刷新令牌刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

3 JWT令牌

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

使用JWT的思路是,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

3.1 令牌的结构

3.1.1 Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
一个例子如下:
下边是Header部分的内容

{
    "alg": "HS256",
    "typ": "JWT"
}

3.1.2 Payload

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。一个例子:

{
    "sub": "1234567890",
    "name": "456",
    "admin": true
}

3.1.3 Signature

第三部分是签名,此部分用于防止jwt内容被篡改。这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明签名算法进行签名。一个例子:

HMACSHA256(
   base64UrlEncode(header) + "." +
   base64UrlEncode(payload),
   secret)

base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:签名所使用的密钥。

4 修改验证服务以颁发JWT令牌

4.1 创建令牌存储

JWTTokenStoreConfig类用于定义Spring将如何管理JWT 令牌的创建、签名和翻译。因为tokenServices()将使用Spring Security的默认令牌服务实现,所以这里的工作是固定的。我们要关注的是jwtAccessTokenConverter()方法,它定义了令牌将如何被翻译。

@Configuration
public class JWTTokenStoreConfig {

    //读取密钥的配置
    @Bean("keyProp")
    public KeyProperties keyProperties(){
        return new KeyProperties();
    }

    @Resource(name = "keyProp")
    private KeyProperties keyProperties;

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory
                (keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray())
                .getKeyPair(keyProperties.getKeyStore().getAlias(),keyProperties.getKeyStore().getPassword().toCharArray());
        converter.setKeyPair(keyPair);
        //配置自定义的CustomUserAuthenticationConverter
        DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
        accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
        return converter;
    }
    @Bean
    @Autowired
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

}

4.2 通过AuthorizationServerConfig类将JWT挂钩到验证服务中

  //授权服务器端点配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                .authenticationManager(authenticationManager)//认证管理器
                .tokenStore(tokenStore)//令牌存储
                .userDetailsService(userDetailsService);//用户信息service
    }

5 在微服务中使用JWT

5.1 将服务配置为受保护资源

@SpringBootApplication
@EnableResourceServer
public class UserApplication {
    public static void main(String[] args) {

        SpringApplication.run(UserApplication.class,args);
    }

}

5.2 ResourceServerConfig定义谁可以访问资源

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    //Http安全配置,对每个到达系统的http请求链接进行校验
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有请求必须认证通过
        http.authorizeRequests()
                //下边的路径放行
                .antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui",
                        "/swagger-resources","/swagger-resources/configuration/security",
                        "/swagger-ui.html","/webjars/**").permitAll()
                .anyRequest().authenticated();
    }
}

5.3 在服务中使用JWT

通过配置JWTTokenStoreConfig,配置JWT存储和翻译。

@Configuration
public class JWTTokenStoreConfig {
    //公钥
    private static final String PUBLIC_KEY = "publickey.txt";

    //定义JwtTokenStore,使用jwt令牌
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    //定义JJwtAccessTokenConverter,使用jwt令牌
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey());
        return converter;
    }
    /**
     * 获取非对称加密公钥 Key
     * @return 公钥 Key
     */
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            return null;
        }
    }
}

5.4 访问测试

不带token,没有权限访问::

带token,可以正常访问:

6 传播OAuth2认证 

需要修改Zuul服务网关,以将0Auth2令牌传播到许可证服务。在默认情况下, Zuul不会将敏感的HTTP首部(如Cookie 、Set-Cookie和Authorization )转发到下游服务。要让Zuul 传播HTTP首部Authorization,需要在Zuul服务网关的application.yml 或Spring Cloud Config 数据存储中设置以下配置:

zuul:
  routes:
    userservice: /user/**
  prefix: /api
  sensitive-headers: Coookie,Set-Cookie

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值