微服务商城系统(十一)权限控制、OAuth 动态加载数据

代码:https://github.com/betterGa/ChangGou

一、资源服务器授权配置

在这里插入图片描述
     基本上所有微服务都是资源服务。
(1)用户微服务
    首先,认证服务使用私钥,采用非对称加密算法 生成令牌,资源服务使用公钥 来校验令牌的合法性。 需要配置公钥,并将公钥拷贝到 public.key 文件中,将此文件拷贝到每一个需要的资源服务工程的 classpath 下 ,比如 用户微服务:
在这里插入图片描述
解析令牌需要添加依赖:

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

    接下来,需要配置每个系统的 Http 请求路径安全控制策略 以及 读取公钥信息识别令牌,对于用户微服务,新建 config 包,提供配置类,@EnableResourceServer 注解用于开启资源校验服务,进行令牌的校验;@EnableGlobalMethodSecurity 注解是全局方法校验:

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

    //公钥
    private static final String PUBLIC_KEY = "public.key";

    /***
     * 定义 JwtTokenStore
     * @param jwtAccessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /***
     * 定义 JJwtAccessTokenConverter
     * @return
     */
    @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;
        }
    }

    /***
     * Http 安全配置,对每个到达系统的 http 请求链接进行校验
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 所有请求必须认证通过
        http.authorizeRequests()
                // 下边的路径放行
                .antMatchers(
                        "/user/add"). //配置地址放行
                permitAll()
                .anyRequest().
                authenticated();    //其他地址需要认证授权
    }
}

     用户每次访问微服务的时候,需要先申请令牌,令牌申请后,每次将令牌放到 Http headers 中,才能访问微服务。Http headers 中每次 需要 添加一个 Authorization 头信息,头的值为 bearer token("bearer"空格 [token])
    比如,先不携带令牌测试,访问 http://localhost:18087 /user ,不携带令牌,结果如下(因为这时网关获取不到令牌,所以响应 401 错误状态码):
在这里插入图片描述
    携带正确令牌访问(这时网关可以获取到令牌信息,予以放行):
在这里插入图片描述
    而且对于 configure 方法中设置的对 user/add 路径放行,可以看到,不需要在 headers 中传 Authorization 参数,确实放行了:
在这里插入图片描述


(2)网关微服务
     接下来,需要让 OAuthorization 对接网关微服务。用户每次访问微服务的时候,先去 OAuth2.0 服务登录,登录后再访问微服务网关,微服务网关将请求转发给其他微服务处理。
**1562729873478**
    在上一篇博客,搭建了 changgou-user-oauth 微服务,实现了 9001/user/login 的登录功能,在上上一篇博客中,搭建了 changgou-gateway-web 微服务,提供了 AuthorizeFilter 全局过滤器,总的来说,实现了以下功能:
(1)用户登录成功(也就是用户名和密码正确)后,会将令牌信息存入到 cookie 中
(2)用户携带 Cookie 中的令牌 访问微服务网关
(3)微服务网关先获取头文件中的令牌信息,如果 HTTP Headers 中没有 Authorization 令牌信息,则到参数中找,参数中如果没有,则到 Cookie 中找,最后将令牌信息封装到 HTTP Headers ,并调用其他微服务
(4)其他微服务会获取 HTTP Headers 中的 Authorization 令牌信息,然后匹配令牌数据是否能使用公钥解密,如果解密成功说明用户已登录,解密失败,说明用户未登录

    修改 changgou-gateway-web 的全局过滤器 AuthorizeFilter 类的逻辑,如果令牌不在 Http Headers 头文件中,需要将 bearer 令牌信息添加到头文件中,而且,现在的令牌是经过私钥生成的,所以是需要用公钥验证,之前的解析JWT的代码不能用啦,先注释掉:
在这里插入图片描述
这时访问 http://localhost:8001/api/user ,将通过私钥生成的新令牌放到头文件中,在令牌前面添加 Bearer 令牌:
在这里插入图片描述
    这里 全局过滤器 AuthorizeFilter的逻辑是,从 Http Headers 中获取到了 Authorization 参数,所以就执行了 chain.filter(exchange) 放行,后续是需要把解析令牌的方法重新提供的。
    而且在 AuthorizeFilter 类中,是依次从头文件、参数、Cookie 中获取 Authorization 参数的,所以一次传过 Authorization 参数,没有清空 Cookie 且未过期的话,后续就不需要传了;或者通过传参数的形式,是不需要加 "bearer " 前缀的,因为代码逻辑里会把 token 取出,再加上"bearer " 前缀放到 Http Headers 中:
在这里插入图片描述

二、SpringSecurity 权限控制

    由于我们项目使用了微服务,任何用户都有可能使用任意微服务,此时我们需要控制相关权限,例如:普通用户角色不能使用用户的删除操作,只有管理员才可以使用,那么这个时候就需要使用到 SpringSecurity 的权限控制功能了。

1、角色加载

    在 changgou-user-oauth 服务中,UserDetailsServiceImpl 类实现了加载用户相关信息:
在这里插入图片描述
    可以看到,给登录用户定义了三个角色,分别为 salesman,accountant,user,目前使用的是硬编码方式将角色写 si 了,后面会从数据库加载。

2、角色权限控制

     在每个微服务中,需要获取用户的角色,然后根据角色识别是否允许操作指定的方法,Spring Security 中定义了四个支持权限控制的表达式注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。在需要控制权限的方法上,我们可以添加 @PreAuthorize注解,用于方法执行前进行权限检查,校验用户当前角色是否能访问该方法。
(1)开启 @PreAuthorize
     之前,在 changgou-user-service 的 ResourceServerConfig 类上 添加了 @EnableGlobalMethodSecurity 注解,用于开启 @PreAuthorize 的支持:
在这里插入图片描述
(2)方法权限控制
     在 UserController 类的 findAll() 方法上添加权限控制注解@PreAuthorize:
在这里插入图片描述
(3)测试
     我们使用 Postman 测试,先创建令牌,然后将 bearer 令牌存放到 Http headers 中,访问 http://localhost:8001/api/user:

1562736397379
     可以看到,发现上面无法访问,因为用户登录的时候,角色不包含 user 角色,所以被拦截了。
     如果希望一个方法能被多个角色访问,在方法上配置 @PreAuthorize("hasAnyAuthority('admin','user')")。如果希望一个类都能被多个角色访问,在类置@PreAuthorize("hasAnyAuthority('admin','user')")
    

三、OAuth 动态加载数据

     前面 OAuth 我们用的数据都是静态,写 si 的:

  • 客户端数据 [生成令牌相关数据]
  • 用户登录账号密码

     在现实工作中,数据应该是从数据库加载的,所以我们需要调整一下 OAuth 服务。
     在 changgou-user-oauth 工程中的 config 包的 AuthorizationServerConfig 类中进行客户端配置,从 数据库中取:
在这里插入图片描述
看看 JdbcClientDetailsService 源码:
在这里插入图片描述
     可以看到,都是对 oauth_client_details 表执行 SQL 语句,所以需要在数据库中提供一个 oauth_client_details 表,用于记录客户端相关信息:
在这里插入图片描述
     上述表结构属于 SpringSecurity Oauth2.0 所需的一个认证表结构,不能随意更改。插入两条记录:

INSERT INTO `oauth_client_details` VALUES ('changgou', null, '$2a$10$wZRCFgWnwABfE60igAkBPeuGFuzk74V2jw3/trkdUZpnteCtJ9p9m', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '432000000', '432000000', null, null);
INSERT INTO `oauth_client_details` VALUES ('szitheima', null, '$2a$10$igxoCZxTbjWx5TrmfWEEpe/WFdwbUhbxik9BKTe9i64ZOSfnu/lqe', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '432000000', '432000000', null, null);

之前的 AuthorizationServerConfig 类中,程序里是写 si 的:
在这里插入图片描述
现在需要改成从数据库中加载用户信息:

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

还需要把 changgou-user-oauth 工程中,config 包下的 UserDetailsServiceImpl 自定义授权 类中, loadUserByUsername 方法的逻辑改成从数据库中加载:

在这里插入图片描述
     可以看到,这时密钥不需要加密处理了,因为是从数据库中查询出来的,已经加密过了。

而且点进紫框里的 clientDatilsService(这个 对象是通过 @Autowired ClientDetailsService clientDetailsService; ·自动装配的),可以看到:
在这里插入图片描述
和 AuthorizationServerConfig 中的 ClientDatilsService 是一个类:
在这里插入图片描述
     也就是说, 这个 AuthorizationServerConfig 的 clientDetails() 方法创建的 从数据库中加载来的 ClientDatailsService 对象,会被 UserDetailsServiceImpl 通过 @Autowired 注解自动装配识别到,而这个对象是会到数据库中加载 oauth_client_details 表 的。
     上一篇博客中,实现了访问 changgou-service-user 用户微服务需要令牌认证,所以,接下来想要既认证客户端 id 和密钥,又认证数据库表中的用户名和密码,就需要通过 changgou-user-oauth 认证微服务去访问用户微服务,进而得到用户的信息,可是这时用户还没登陆呢,所以是没有令牌的,理应是被禁止访问的,我们先对 findById 方法进行放行,不用令牌也能访问。
先加个路径:
在这里插入图片描述
进行放行:
在这里插入图片描述
再给 OAuthApplication 类上开启 feign:
在这里插入图片描述
     后续就可以进行 feign 调用啦,在 service-api 中提供 feign :

@FeignClient(value = "user")
@RequestMapping(value = "/user")
public interface UserFeign{

    /**
     * 查询用户信息
     * @param id
     * @return
     */
    @GetMapping({"/load/{id}"})
    Result<User> findById(@PathVariable String id);
}

     就可以在 UserDetailsServiceImpl 类中进行 user 的 feign 调用,整个 UserDetailsServiceImpl 的代码如下:

/*****
 * 自定义授权认证类
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    ClientDetailsService clientDetailsService;

    @Autowired
    UserFeign userFeign;

    /****
     * 自定义授权认证
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        /***
         * 客户端信息认证
         */
        // (3)
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // (4)
        if (authentication == null) {
        
        // (1)
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if (clientDetails != null) {
                //秘钥
                String clientSecret = clientDetails.getClientSecret();

                /* 静态方式*/
                //return new User(username, // 客户端 ID
                // new BCryptPasswordEncoder().encode(clientSecret), // 客户端密钥
                // AuthorityUtils.commaSeparatedStringToAuthorityList(""));

                // (2)
                return new User(username, // 客户端 ID
                        clientSecret, // 客户端密钥
                        AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }

        /***
         * 用户账号密码信息认证
         */
        if (StringUtils.isEmpty(username)) {
            return null;
        }

        // (5) 从数据库加载用户信息
        Result<pojo.User> userResult = userFeign.findById(username);

        if (userResult == null || userResult.getData() == null) {
            return null;
        }

        // 获取用户名对应的密码
        String password = userResult.getData().getPassword();

        // String pwd = new BCryptPasswordEncoder().encode(password);
        // 指定用户角色信息
        String permissions = "salesman,accountant,user";


        UserJwt userDetails = new UserJwt(username, password, AuthorityUtils
                .commaSeparatedStringToAuthorityList(permissions));

        return userDetails;
    }
}

     分析一下这里的逻辑:访问 http://localhost:9001/user/login?username=szitheima&password=szitheima 路径,是先执行的 UserLoginController 控制类的 login 方法,这个方法是从 application.yml 的 auth:clientId 属性里拿到的 username “changgou”,刚开始,(4)是 null 的,然后会执行(1),(1)会把这个 username 传递给 loadUserByUsername 方法,先进行客户端信息认证,会先到 clientDetailsService.loadClientByClientId 方法,这个前面提到了,实现类是 JdbcClientDetailsService ,所以是会到数据库的 oauth_client_details 表中查询 client_id, client_secret, scope 等信息(不是从 application.yml 中得到的喔),把这些信息作为 ClientDetails 对象的属性,因为 clientid 是 changgou,所以查到的都是 changgou 对应的信息。不过 (2)处,并不是直接返回 new 的 User 对象,会去执行 DaoAuthenticationProvider 类的 retrieveUser 方法、ProviderManager类的 authenticate 方法、BasicAuthenticationFilter 类的 doFilterInternal 方法(这个方法里,username 不是客户端的那个 “changgou” 了,而是 login 路径里的 username 参数 “szitheima”),然后又会跳回 (3)处重新执行一遍,这时 (4)就不是 null 了,会到(5),加载数据库中的信息,这时的 username 是 szitheima,然后用查询到的 username, password, permissions 信息,作为 JWT 的载荷,返回结果。

     (代码没细研究,不过感觉有点儿 Spring bean 一二级缓存的那个意思,暂时没想到为什么这么设计,有待细究。)

接下来进行用户账号信息认证,通过用户名查询用户记录,并把用户名、密码作为生成令牌的依据,生成令牌,运行效果:
在这里插入图片描述
(可以把生成的令牌给到 testParseToken() 方法,进行解析,运行结果:
在这里插入图片描述
可以看到,封装了用户名、角色等信息。以后自己需要生成令牌时,可以参考这个令牌中的参数。)
    
doFilterInternal 方法 源码:

 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
 		// (1)
        boolean debug = this.logger.isDebugEnabled();
        String header = request.getHeader("Authorization");
        if (header != null && header.toLowerCase().startsWith("basic ")) {
            try {
                String[] tokens = this.extractAndDecodeHeader(header, request);

                assert tokens.length == 2;

                String username = tokens[0];
                if (debug) {
                    this.logger.debug("Basic Authentication Authorization header found for user '" + username + "'");
                }

                if (this.authenticationIsRequired(username)) {
                    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, tokens[1]);
                    authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
                    Authentication authResult = this.authenticationManager.authenticate(authRequest);
                    if (debug) {
                        this.logger.debug("Authentication success: " + authResult);
                    }

                    SecurityContextHolder.getContext().setAuthentication(authResult);
                    this.rememberMeServices.loginSuccess(request, response, authResult);
                    this.onSuccessfulAuthentication(request, response, authResult);
                }
            } catch (AuthenticationException var10) {
                SecurityContextHolder.clearContext();
                if (debug) {
                    this.logger.debug("Authentication request for failed: " + var10);
                }

                this.rememberMeServices.loginFail(request, response);
                this.onUnsuccessfulAuthentication(request, response, var10);
                if (this.ignoreFailure) {
                    chain.doFilter(request, response);
                } else {
                    this.authenticationEntryPoint.commence(request, response, var10);
                }

                return;
            }

            chain.doFilter(request, response);
        } else {
            chain.doFilter(request, response);
        }
    }

四、总结

(1) 基本上所有微服务都是资源服务。
     资源服务,比如 user 工程,使用公钥来验证令牌,需要在 resources 下提供 public.key,导入 spring-cloud-starter-oauth2 依赖,提供使用 @Configuration 声明配置类,并在配置类上使用 @EnableResourceServer 开启资源服务。并在其中提供生成 TokenStore、JwtAccessTokenConverter、publicKey 的方法,以及在 configure(HttpSecurity)方法中设置请求需要通过认证、可以放行的路径。所以,访问 user 工程对应的 18087 端口,需要在 headers 中提供 key 为 Authorization,value 为 bearer token 的参数。
    
(2)使用 Spring Security 权限控制:
     需要先在实现了 UserDetailsService 的类中,指定角色,比如 “salesman,accountant,user”,覆写 loadUserByUsername 方法,返回值为 UserDetails,把角色封装在 UserDetails 对象中。
     比如,findAll 方法仅限 user 角色访问,需要在 findAll 方法上使用 @PreAuthorize(“hasRole(‘user’)”)注解,并在相应的启动类上使用 @EnableGlobalMethodSecurity 注解开启对 @PreAuthorize 的支持。
    
(3)使用 SpringSecurity Oauth2.0 从数据库中动态加载用户数据。
     首先需要在数据库中提供 oauth_client_details 认证表,包含客户端 ID、客户端密钥、令牌有效期等字段,表结构不能随意更改。
     在继承了 AuthorizationServerConfigurerAdapter 的配置类中提供:

 @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }
 @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.jdbc(dataSource).clients(clientDetails());
    }

     在实现了 UserDetailsService 接口的 Service 类中,loadUserByUsername 逻辑需要改成从数据库中加载,并且在这个方法里进行用户信息的认证,即 通过 Feign 调用 user 工程里的 findById 方法,通过 username 这个 ID,查询用户信息,获取到用户名对应的密码,将用户名、密码、角色 作为载荷,
生成 JWT 返回 。

🎉 bearer token

    这是 RFC 6750 的链接:https://tools.ietf.org/html/rfc6750,是对 bearer token 的规范。定义是这样的:
    bearer token 是一种安全令牌,具有以下属性:拥有 bearer token 的任何一方(被称为 “bearer”),可以以任何方式,和 同样持有它的任何一方 一样地使用它 来访问受 OAuth 2.0 保护的资源(但是不能 也不需要证明 bearer 有加密用的密钥),为了保护 bearer token 不被误用,需要保证它在存储和传输过程中不被泄露。bearer 认证方案主要用于使用了 WWW-Authenticate 和 Authorization HTTP headers 的服务。
    我的理解是 bearer token 就像我们用的百度网盘,不需要知道网盘加密算法,只需要提取码就可以访问资源。
    在 user 微服务中,ResourceServerConfig 类 继承了ResourceServerConfigurerAdapter 类,源码如下:
在这里插入图片描述
    有两个 configure 方法,第一个的参数与资源安全配置相关,第二个与 http 安全配置相关。
在这里插入图片描述
    在这里设置了 TokenExtractor 默认的实现—-BearerTokenExtractor,所以,访问用户微服务时,Http headers 的 Authorization 参数里的 "bearer " 是必不可少的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值