springcloudOauthSpring Cloud

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

2022-04-13  本文已影响0人  天还下着毛毛雨
image.png

1、前言

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

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

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

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

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

2、JWT简介

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

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

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。
上一篇下一篇

猜你喜欢

热点阅读