美文网首页springcloudOauthSpring Cloud
13. SpringCloud之集成Oauth2.0权限校验之J

13. SpringCloud之集成Oauth2.0权限校验之J

作者: 天还下着毛毛雨 | 来源:发表于2022-04-13 22:44 被阅读0次
    image.png

    1、前言

    前面 讲到, oauth2.0不管是 客户端模式,密码模式还是授权码模式, token由于 不承载任何 信息, 所以 token的合法性校验以及用户的权限信息 只能通过调 认证服务器 的 检查接口 才能获取到。在微服务中,所有需要权限校验的接口 都多了一次 检查token接口的 调用 ,这就对 接口的响应速度有一定影响了。

    那么有什么办法 可以 省略掉 这步 向认证服务器 请求检验token,获取用户信息的接口调用呢?

    答案 就是 认证服务器持有私钥, 利用 jwt 结合 非对称加密算法(如RSA) 生成 一个 承载了用户信息的 token。资源服务器 通过 与 认证服务器相互匹配的 公钥 ,进行解密 ,让资源服务器自己来校验 token的合法性 ,并且从中 解析到 用户的权限信息,从而做更细致的权限校验。

    也可以采用对称加密 算法, 认证服务器和 资源服务器 持有 相同的密钥 就可以 对 token进行 加解密了。

    这样就可以 不用再请求 认证服务器 校验token,获取用户信息了。

    2、JWT简介

    全名 json web token,是一种无状态的权限认证方式,一般用于前后端分离,时效性比较短的权限校验。

    Jwt 的 token 信息分成三个部分,用“.”号分割的。

    • 第一部分:头信息,通过 base64 加密生成 ,包含加密的算法, token的类型。
    • 第二部分:有效载荷(用户信息),通过 base64 加密生成 。
    • 第三部分:签名,根据头信息中的加密算法通过,RSA(base64(头信息) + “.” + base64(有效载 荷))生成的第三部分内容。

    jwt官网(jwt.io)示例:

    3、SpringCloud集成outh2.0使用Jwt鉴权

    除了对jwt配置的一些信息意外,其他大部分代码配置 都和之前的密码模式 差不多。

    3.1.生成公钥 和 私钥

    3.1.1、私钥

    cd 到 jdk 的 bin 目录执行该指令

    # oauth-jwt-server 是私钥文件名称
    keytool -genkeypair -alias oauth-jwt-server
          -validity 3650
            -keyalg RSA
          -dname "CN=jwt,OU=jtw,O=jwt,L=zurich,S=zurich, C=CH"
            -keypass 123456
            -keystore oauth-jwt-server.jks
          -storepass 123456
    

    会在 bin 目录下生成 oauth-jwt-server.jks 文件

    image.png

    把这个文件放到认证服务器的resouces目录下

    一会 用代码 定义jwt 转换器的时候 需要加载它。

    image.png

    3.1.2、公钥

    针对 私钥文件生成 与之配置的公钥

    oauth-jwt-server.jks 是刚刚生成的 私钥文件

    keytool -list -rfc --keystore oauth-jwt-server.jks | openssl x509 -inform pem -pubkey
    

    会返回 一串 公钥

    image.png

    在资源服务器的resouces目录下 新建一个public.cert,文件内容就是上面的这串公钥。

    image.png

    3.2、认证服务器代码配置

    3.2.1、认证服务器配置

    认证服务器配置里 和 密码模式不一样的 只有 配置token的部分, 其他都一模一样。

    @Configuration
    @EnableAuthorizationServer
    public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        @Qualifier("authenticationManagerBean")
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserServiceDetail userServiceDetail;
    
        @Autowired
        private DataSource dataSource;
    
        @Bean // 声明 ClientDetails实现
        public ClientDetailsService clientDetailsService() {
            return new JdbcClientDetailsService(dataSource);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(clientDetailsService());
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            // 配置token的存储方式为JwtTokenStore
            endpoints.tokenStore(tokenStore())
                    // 配置用于JWT私钥加密的增强器
                    .tokenEnhancer(jwtTokenEnhancer())
                    // 配置安全认证管理
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userServiceDetail);
        }
    
        // token存储采用 JwtTokenStore
        @Bean
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtTokenEnhancer());
        }
    
        @Bean
        protected JwtAccessTokenConverter jwtTokenEnhancer() {
            // 配置jks文件
            KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth-jwt-server.jks"), "123456".toCharArray());
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth-jwt-server"));
            return converter;
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            // 对获取Token的请求不再拦截
            oauthServer.tokenKeyAccess("permitAll()")
                    // 验证获取Token的验证信息
                    .checkTokenAccess("isAuthenticated()");
        }
    }
    

    3.2.2、其他的配置 (和密码模式一样)

    UserServiceDetail类, 安全配置,pom依赖 ,配置文件 相较于之前的密码模式,都没有任何变化

    1、UserServiceDetail(和密码模式一样)

    还是用jpa从 数据里 查询出用户权限信息

    @Service
    public class UserServiceDetail implements UserDetailsService {
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return userRepository.findByUsername(username);
        }
    }
    

    2、安全配置(和密码模式一样)

    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private UserServiceDetail userServiceDetail;
    
        @Override
        public @Bean
        AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        /*
        *   access(String) 如果给定的SpEL表达式计算结果为true,就允许访问
            anonymous() 允许匿名用户访问
            authenticated() 允许认证的用户进行访问
            denyAll() 无条件拒绝所有访问
            fullyAuthenticated() 如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问
            hasAuthority(String) 如果用户具备给定权限的话就允许访问
            hasAnyAuthority(String…)如果用户具备给定权限中的某一个的话,就允许访问
            hasRole(String) 如果用户具备给定角色(用户组)的话,就允许访问/
            hasAnyRole(String…) 如果用户具有给定角色(用户组)中的一个的话,允许访问.
            hasIpAddress(String 如果请求来自给定ip地址的话,就允许访问.
            not() 对其他访问结果求反.
            permitAll() 无条件允许访问
            rememberMe() 如果用户是通过Remember-me功能认证的,就允许访问
        *
        * */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();//关闭CSRF
    //                .exceptionHandling()
    //                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
    //                .and()
    //                .authorizeRequests()
    //                .antMatchers("/oauth/**").permitAll()
    ////                .antMatchers("/**").authenticated()
    //                .and()
    //                .httpBasic();
            http.requestMatchers().anyRequest()
                    .and()
                    .authorizeRequests()
                    .antMatchers("/oauth/**").permitAll();
        }
    
        @Bean
        PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    
        @Bean
        public static NoOpPasswordEncoder noOpPasswordEncoder() {
            return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userServiceDetail).passwordEncoder(passwordEncoder());
        }
    }
    

    3、jwt相关pom(和密码模式一样)

    <!--oauth 2.0相关 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-data</artifactId>
    </dependency>
    <!-- 持久化 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    

    4、配置文件(和密码模式一样)

    # eureka客户端配置
    spring.application.name=oauth-jwt-server
    #spring.cloud.controller.uri= http://localhost:9009/
    server.port=9062
    #eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
    eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9001/eureka/
    
    management.endpoints.web.exposure.include=*
    
    # 数据源
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://maomaoyu.xyz:3306/oauth2.0?serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=123456
    
    logging.level.org.springframework.security=debug
    
    # jpa配置
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.show-sql=true
    # 开启basic 认证
    spring.security.basic.enabled=true
    

    3.2.3、请求获取jwt token

    1、请求头Authorization (和密码模式一样)

    在请求头Authorization 设置 Basic 加密后的客户端 名称和密码

    image.png

    可以看到 headers里面已经生成了Authorization请求头了

    image.png

    2、body参数,表单提交 (和密码模式一样)

    输入鉴权类型grant_type 和 用户名密码,scope

    image.png

    3、成功获取token

    image.png

    可以看到,jwt生成的token 由于 含有的信息比较多,相较于 oauth2.0 其他方式的token会长的多。

    把这串放到jwt官网,可以成功解析中 里面的信息

    image.png

    最终是否校验成功,要看 输入的公钥 是都匹配, 匹配的话 会显示 签名校验成功

    image.png

    不匹配的话 (在填入的公钥后面乱加了点东西),会显示签名校验失败。


    image.png

    3.3、资源服务器配置

    3.3.1、资源服务器配置

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        @Autowired
        private TokenStore tokenStore;
    
        @Value("${spring.application.name}")
        private String applicationName;
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.csrf().disable()
                    .authorizeRequests()
                    .antMatchers("/**").authenticated();
        }
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            super.configure(resources);
    //        resources.authenticationEntryPoint(new RefreshTokenAuthenticationEntryPoint());
            // 把sping实例名称作为当前资源服务器 resourceId
            resources.tokenStore(tokenStore).resourceId(applicationName);
        }
    }
    

    3.3.2、jwt配置

    配置token存储方式为jwt,并配置 jwt转换器,加载 公钥 ,用于解密jwt token

    @Configuration
    public class JwtConfig {
    
        @Autowired
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Bean
        @Qualifier("tokenStore")
        public TokenStore tokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter);
        }
    
        @Bean
        public JwtAccessTokenConverter jwtTokenEnhancer() {
            // 用作JWT转换器
            JwtAccessTokenConverter converter =  new JwtAccessTokenConverter();
           // 加载 公钥  
          Resource resource = new ClassPathResource("public.cert");
            String publicKey;
            try {
                publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            //设置公钥
            converter.setVerifierKey(publicKey);
            return converter;
        }
    }
    

    3.3.3、启动 方法级别鉴权(和密码模式一样)

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class GlobalMethodSecurityConfig {
    }
    

    3.3.4、配置文件

    配置认证服务器 的 接口(这个接口是Oauth2.0框架自带的), 资源服务器启动的时候会 调一次这个接口 看是否正常

    security.oauth2.resource.jwt.key-uri=http://localhost:9062/oauth/token_key
    
    #由于token的合法性和token里面的用户信息 由资源服务器自己利用 公钥解析,不要再配置这个去请求认证服务器了。
    #security.oauth2.resource.user-info-uri=http://127.0.0.1:9052/security/check
    

    3.3.5、其他配置(和密码模式一样)

    pom依赖 和 oauth2.0其他模式没有任何差别 ,其他的配置也没变化

    1、pom依赖(和密码模式一样)

    <!--oauth 2.0相关 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-data</artifactId>
    </dependency>
    <!-- 持久化 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    

    2、配置文件(和密码模式一样)

    spring.application.name=oauth-jwt-server
    #spring.cloud.controller.uri= http://localhost:9009/
    server.port=9062
    #eureka.client.service-url.defaultZone=http://localhost:9001/eureka/
    eureka.client.serviceUrl.defaultZone=http://admin:admin@localhost:9001/eureka/
    
    spring.redis.database=1
    spring.redis.host=maomaoyu.xyz
    spring.redis.port=6379
    spring.redis.password=123456
    
    management.endpoints.web.exposure.include=*
    
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://maomaoyu.xyz:3306/oauth2.0?serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=123456
    
    logging.level.org.springframework.security=debug
    
    
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.show-sql=true
    
    spring.security.basic.enabled=true
    

    3.3.6、使用token

    把token放到Authorization请求头里, 内容为 Bearer+ 空格 + jwt的token,

    image.png

    最终可以看到,请求成功

    image.png

    4、jwt的弊端

    1. 由于jwt是无状态的(不在后台任何地方存储), 如果token被人盗取的话(比如抓包,浏览器F12),那么 将无法 阻止这个token 被非法使用(像有状态的token 放在redis或者数据库里,可以通过删除 这个token 来让其无法访问)。 唯一的办法 只能用https, 让token 没法被别人抓取。
    2. token太长,相较于 oauth2.0默认的token,放在请求头里,占用的网络带宽 会 增大。
    3. 无法续期,jwt 的token生成出来过期时间就已经 加密在 token 中了,没法像redis存储的session或者token一样,通过更新 过期时间来延长session/ token的有效期。到期了就只能通过refresh_token 或者 再次登录 重新生成token。

    相关文章

      网友评论

        本文标题:13. SpringCloud之集成Oauth2.0权限校验之J

        本文链接:https://www.haomeiwen.com/subject/beszsrtx.html