• 使用JWT作为Spring Security OAuth2的token存储


    Spring Security OAuth2的demo在前几篇文章中已经讲过了,在那些模式中使用的都是RemoteTokenService调用授权服务器来校验token,返回校验通过的用户信息供上下文中获取

    这种方式会加重授权服务器的负载,你想啊,当用户没授权时候获取token得找授权服务器,有token了访问资源服务器还要访问授权服务器,相当于说每次请求都要访问授权服务器,这样对授权服务器的负载会很大

    常规的方式有两种来解决这个问题:

    1. 使用JWT作为Token传递
    2. 使用Redis存储Token,资源服务器本地访问Redis校验Token

    使用JWT与Redis都可以在资源服务器中进行校验Token,从而减少授权服务器的工作量

    JWT默认使用HMACSHA256对称加密算法,以下记录下默认算法实现与非对称RSA算法的集成,使用不同算法加解密测试方法是一致的,所以放在文章最后

    授权服务器整合JWT——对称加解密算法

    授权服务器整体代码结构

    pom.xml中引入依赖

    	<dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
                <version>2.2.1.RELEASE</version>
            </dependency>        
    		<!-- Spring Security OAuth2 -->
            <dependency>
                <groupId>org.springframework.security.oauth</groupId>
                <artifactId>spring-security-oauth2</artifactId>
                <version>2.4.0.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.1.0.RELEASE</version>
            </dependency>
    

    SecurityConfig配置,主要需要显式声明AuthenticationManager和UserDetailsService这两个bean

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
        @Bean
        public UserDetailsService userDetailsService(){ //主要是配置这个Bean,用于授权服务器配置中注入
            return super.userDetailsService();
        }
    
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // @formatter: off
            auth.inMemoryAuthentication()
                    .withUser("hellxz")
                    .password(passwordEncoder().encode("xyz"))
                    .authorities(Collections.emptyList());
            // @formatter: on
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated() //所有请求都需要通过认证
                    .and()
                    .httpBasic() //Basic提交
                    .and()
                    .csrf().disable(); //关跨域保护
        }
    }
    

    授权服务器配置AuthorizationConfig

    @Configuration
    @EnableAuthorizationServer //开启授权服务
    public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        public UserDetailsService userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            //允许表单提交
            security.allowFormAuthenticationForClients()
                    .checkTokenAccess("permitAll()")
                    .tokenKeyAccess("permitAll()");
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            // @formatter: off
            clients.inMemory()
                    .withClient("client-a") //client端唯一标识
                        .secret(passwordEncoder.encode("client-a-secret")) //client-a的密码,这里的密码应该是加密后的
                        .authorizedGrantTypes("authorization_code", "password", "refresh_token") //授权模式标识,这里主要测试用password模式,另外refresh_token不是一种模式,但是可以使用它来刷新access_token(在它的有效期内)
                        .scopes("read_user_info") //作用域
                        .resourceIds("resource1") //资源id
                        .redirectUris("http://localhost:9001/callback"); //回调地址
    
            // @formatter: on
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService)
                    .tokenStore(jwtTokenStore()) //设置jwtToken为tokenStore
                    .accessTokenConverter(jwtAccessTokenConverter());//设置access_token转换器
        }
    
        /**
         * jwt访问token转换器
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(){
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey("my-sign-key"); //资源服务器需要配置此选项方能解密jwt的token
            return converter;
        }
    
        /**
         * jwt的token存储对象
         */
        @Bean
        public JwtTokenStore jwtTokenStore(){
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    }
    

    这里主要是在configure(AuthorizationServerEndpointsConfigurer endpoints)授权服务的端点配置中加入JWT的tokenStore和access_token的转换器,以及这二者的声明Bean方法

    这里使用的是默认对称MAC算法,即加密解密使用相同的密钥

    启动类就不说了,开启@SpringBootApplicatin的main方法

    资源服务器整合JWT——对称加解密算法

    资源服务器主要就一个资源配置类

    @Configuration
    @EnableResourceServer
    public class ResourceConfig extends ResourceServerConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //设置创建session策略
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
            //@formatter:off
            //所有请求必须授权
            http.authorizeRequests()
                    .anyRequest().authenticated();
            //@formatter:on
        }
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) {
            resources.tokenStore(jwtTokenStore());
        }
    
        /**
         * jwt访问token转换器
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(){
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey("my-sign-key"); //与授权服务器相同的signingKey
            return converter;
        }
    
        /**
         * jwt的token存储对象
         */
        @Bean
        public JwtTokenStore jwtTokenStore(){
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
    }
    

    配置JWT的TokenStore和AccessTokenConverter与授权服器相同,添加启动类完成配置

    OAuth整合JWT——非对称加解密RSA

    本部分基于对称加密部分,仅展示需要修改的部分

    首先使用keytool生成jks (Java Key Store) 密钥,按提示输入姓氏等信息

    keytool -genkeypair -alias hellxz-jwt -validity 3650 -keyalg RSA -keypass hellxzTest -keystore hellxz-jwt.jks -storepass hellxzTest
    

    生成的私钥文件会在当前目录,把hellxz-jwt.jks复制到授权服务器的resources目录下
    授权服务器需修改jwtAccessTokenConverter()

        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(){
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            KeyStoreKeyFactory storeKeyFactory = new KeyStoreKeyFactory(
                    new ClassPathResource("hellxz-jwt.jks"), "hellxzTest".toCharArray());
            converter.setKeyPair(storeKeyFactory.getKeyPair("hellxz-jwt"));
            return converter;
        }
    

    在hellxz-jwt.jks同目录下,执行命令生成公钥

    ➜ keytool -list -rfc --keystore hellxz-jwt.jks | openssl x509 -inform pem -pubkey
    输入密钥库口令:  hellxzTest
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxU7zulFUVBXmZD28xwM4
    ul5e9yFrToLgWKHlNLlp904/GbiWBoZ4tcBcNq3VxLGBN9VOqfP1P5C7fRgz95UI
    7ShKCKgsFFGL2rAqsplMDClN/adfsxmpF06rVIkGgce9tR0Q0iONcaN+b/lArK4T
    Au76QsQwn9MLXlznVfczclZOZSfDNju+1JuBzqt6fEPWqalBUVYdV0zCUDG8ikN1
    l9D0m1tSSaKpiTrU2yEUGUji+79Ury7Y8BClEX6d4CTl9TQAhL5g32GoJEc0S2y+
    0bqeqUsv1nUt9KiJT9kiOvA+Q7o2T8OHuqQT9le7kvmIi4gSX5vSNvvZagE2Uglh
    zQIDAQAB
    -----END PUBLIC KEY-----
    -----BEGIN CERTIFICATE-----
    MIIDUTCCAjmgAwIBAgIEePeDczANBgkqhkiG9w0BAQsFADBZMQswCQYDVQQGEwJD
    TjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEKMAgGA1UEChMB
    MDEKMAgGA1UECxMBMDEOMAwGA1UEAxMFemhhbmcwHhcNMTkxMjE1MDUyOTM2WhcN
    MjkxMjEyMDUyOTM2WjBZMQswCQYDVQQGEwJDTjEQMA4GA1UECBMHYmVpamluZzEQ
    MA4GA1UEBxMHYmVpamluZzEKMAgGA1UEChMBMDEKMAgGA1UECxMBMDEOMAwGA1UE
    AxMFemhhbmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDFTvO6UVRU
    FeZkPbzHAzi6Xl73IWtOguBYoeU0uWn3Tj8ZuJYGhni1wFw2rdXEsYE31U6p8/U/
    kLt9GDP3lQjtKEoIqCwUUYvasCqymUwMKU39p1+zGakXTqtUiQaBx721HRDSI41x
    o35v+UCsrhMC7vpCxDCf0wteXOdV9zNyVk5lJ8M2O77Um4HOq3p8Q9apqUFRVh1X
    TMJQMbyKQ3WX0PSbW1JJoqmJOtTbIRQZSOL7v1SvLtjwEKURfp3gJOX1NACEvmDf
    YagkRzRLbL7Rup6pSy/WdS30qIlP2SI68D5DujZPw4e6pBP2V7uS+YiLiBJfm9I2
    +9lqATZSCWHNAgMBAAGjITAfMB0GA1UdDgQWBBQF96rK7n0XufnvtJuH9tD9Ixza
    6zANBgkqhkiG9w0BAQsFAAOCAQEAuMzWZJhej6+4TGgodQKQ5L5RBtOUbesxA1Ue
    s9iA4m/jNZnVCXJE0nY47YVzBCIkIsYALswGooMj1PIJxEMpggXVmIuiJpaPgg+4
    sthzISxKzX0ru8IrJTapaglMi74ai6S73LTBSke9GEPgWWnbtdUZoUSiSNt1oJ0J
    EhFHdPuzxc36neDFRBOBxW4w3qhsTlKTN2wJm1nLV96nFKmqJhQJhhKt6ihe7hMg
    qWxzNsWAqv9gJNdKZt5teqwNKT6H7r1NX5oJkJ0Kn1dZy0O3rDDd5E0KDKkMtwOh
    3deJH6Uvtt/dw/drzJlByNDEPp6hYGQu2dW5JG5uiHuzFHnJeA==
    -----END CERTIFICATE-----
    
    

    复制公钥部分到public.cert放到资源服务器的resources目录

    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxU7zulFUVBXmZD28xwM4
    ul5e9yFrToLgWKHlNLlp904/GbiWBoZ4tcBcNq3VxLGBN9VOqfP1P5C7fRgz95UI
    7ShKCKgsFFGL2rAqsplMDClN/adfsxmpF06rVIkGgce9tR0Q0iONcaN+b/lArK4T
    Au76QsQwn9MLXlznVfczclZOZSfDNju+1JuBzqt6fEPWqalBUVYdV0zCUDG8ikN1
    l9D0m1tSSaKpiTrU2yEUGUji+79Ury7Y8BClEX6d4CTl9TQAhL5g32GoJEc0S2y+
    0bqeqUsv1nUt9KiJT9kiOvA+Q7o2T8OHuqQT9le7kvmIi4gSX5vSNvvZagE2Uglh
    zQIDAQAB
    -----END PUBLIC KEY-----
    

    修改资源服务器jwtAccessTokenConverter()方法

        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter(){
            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;
        }
    

    测试验证

    发送POST请求http://localhost:8080/oauth/token?username=hellxz&password=xyz&scope=read_user_info&grant_type=password

    返回结果

    带token访问资源服务器

    测试通过

    另外使用JWT应设置尽量短的过期时间,因为JWT的token无法手动revoke,只能等待其到达过期时间失效

  • 相关阅读:
    python模块——socket (实现简单的C/S架构端通信操作CMD)
    English trip -- VC(情景课)3 C Do you have a sister?(maple verstion)
    防止文件被恢复
    English trip -- MC(情景课)3 C Do you have a sister?
    English trip -- VC(情景课)3 B Bamily members
    English trip -- MC(情景课)6 Time
    TCP三次握手(待细研究)
    English trip -- VC(情景课)3 A Family
    English trip -- Phonics 4 元音字母 i
    心情随笔20180718
  • 原文地址:https://www.cnblogs.com/hellxz/p/12044340.html
Copyright © 2020-2023  润新知