1. 程式人生 > 其它 >Spring Security OAuth2使用Redis作為token儲存

Spring Security OAuth2使用Redis作為token儲存

Spring Security OAuth2使用Redis作為token儲存

授權application.yml伺服器儲存token到Redis

server:
  port: 8080

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123
    database: 0 #也可以在程式碼中指定

Maven依賴

      <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.62</version>
        </dependency>

在spring security oauth2中,授權服務使用redis儲存token的時候,報錯:

java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V

這說明版本有問題,解決方案是,將oauth2的版本升級到2.3.3,即在pom檔案中,加入:

<!-- oauth2 start -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <!-- 指明版本,解決redis儲存出現的問題:java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V問題 -->
    <version>2.3.3.RELEASE</version>
</dependency>
<!-- oauth2 end -->

程式碼分為認證端和客戶端兩個服務

認證端,也就是我的security服務

有兩個檔案,一個配置問津,一個

1.AuthResourcesConfig

package com.adao.security.config;

import com.adao.security.common.AuthResourcesConfig;
import com.adao.security.service.SSOUserDetailsService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
* @author * @version 1.0 * @date 2021/8/12 * @Description: 資源服務資訊公共配置類 */ public class AuthResourcesConfig { /** * token過期時間,單位為秒 */ public static final int TOKEN_SECONDS_ACCESS = 10 * 60; /** * 重新整理token時間,單位為秒 */ public static final int TOKEN_SECONDS_REFRESH = 10 * 60; /** * 資源服務客戶端ID資訊 */ //rtp public static final String CLIENT_RTP = "RTP"; //manage public static final String CLIENT_RTP_MANAGE = "adao-rtp-manage"; /** * 資源節點對應的金鑰,目前統一為 adao */ public static final String CLIENT_RTP_SECRET = "adao"; }

2.AuthorizationServerConfig

package com.adao.security.config;

import com.adao.security.common.AuthResourcesConfig;
import com.adao.security.service.SSOUserDetailsService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;


/**
 * @author adao
 * @version 1.0
 * @Description: 認證伺服器配置
 * @date 2021/8/11
 */
@Configuration
@EnableAuthorizationServer
@Log4j2
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 鑑權模式
     */
    public static final String GRANT_TYPE[] = {"password", "refresh_token"};

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 使用者服務
     */
    @Autowired
    public SSOUserDetailsService userDetailsService;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * Redis資料庫存
     */
    public static final int REDIS_CONNECTION_DATABASE = 14;

    /**
     * 客戶端資訊配置,可配置多個客戶端,可以使用配置檔案進行代替
     *
     * @param clients 客戶端設定
     * @throws Exception 異常
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 定義客戶端應用的通行證
        clients.inMemory()
                // rtp-manage
                .withClient(AuthResourcesConfig.CLIENT_RTP_MANAGE)
                .secret(new BCryptPasswordEncoder().encode(AuthResourcesConfig.CLIENT_RTP_SECRET))
                .authorizedGrantTypes(GRANT_TYPE[0], GRANT_TYPE[1])
                .scopes("all")
                .autoApprove(true)

                // rtp
                .and()
                .withClient(AuthResourcesConfig.CLIENT_RTP)
                .secret(new BCryptPasswordEncoder().encode(AuthResourcesConfig.CLIENT_RTP_SECRET))
                .authorizedGrantTypes(GRANT_TYPE[0], GRANT_TYPE[1])
                .scopes("all")
                .autoApprove(true);

    }

    /**
     * 配置端點
     *
     * @param endpoints 端點
     * @throws Exception 異常
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置認證管理器
        endpoints.authenticationManager(authenticationManager)
                //配置使用者服務
                .userDetailsService(userDetailsService)
                //配置token儲存的服務與位置
                .tokenServices(tokenService())
                .tokenStore(redisTokenStore());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        //允許使用者訪問OAuth2 授權介面
        security.allowFormAuthenticationForClients();
        //允許已授權使用者訪問 checkToken 介面和獲取 token 介面
        security.tokenKeyAccess("permitAll()");
        //允許已授權使用者獲取 token 介面
        security.checkTokenAccess("permitAll()");
    }

    /**
     * 設定token儲存,資源伺服器配置與此處相一致
     */
    @Bean
    public RedisTokenStore redisTokenStore() {
        // 指定redis資料庫儲存token,與業務庫區分。
        LettuceConnectionFactory lettuceConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
        lettuceConnectionFactory.setDatabase(REDIS_CONNECTION_DATABASE);
        RedisTokenStore redisTokenStore = new RedisTokenStore(lettuceConnectionFactory);
// 也可以用在何種方式,這樣用哪個資料庫是在配置檔案中指定    
//RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
log.info("Oauth2 redis database : [{}]", lettuceConnectionFactory.getDatabase()); //設定redis token儲存中的字首 redisTokenStore.setPrefix("auth-token:"); return redisTokenStore; } @Bean public DefaultTokenServices tokenService() { DefaultTokenServices tokenServices = new DefaultTokenServices(); //配置token儲存  tokenServices.setTokenStore(redisTokenStore()); //開啟支援refresh_token tokenServices.setSupportRefreshToken(true); //複用refresh_token tokenServices.setReuseRefreshToken(true); //token有效期  tokenServices.setAccessTokenValiditySeconds(AuthResourcesConfig.TOKEN_SECONDS_ACCESS); //refresh_token有效期  tokenServices.setRefreshTokenValiditySeconds(AuthResourcesConfig.TOKEN_SECONDS_REFRESH); return tokenServices; } }

接下來是資源端

1.ResourceServerConfig

package com.adao.manage.config;

import com.adao.manage.common.AuthExceptionHandler;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

/**
 * @author adao
 * @version 1.0
 * @date 2021/8/11
 * @description 資源服務配置類
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Log4j2
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Autowired
    private AuthExceptionHandler authExceptionHandler;

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * Redis資料庫存
     */
    public static final int REDIS_CONNECTION_DATABASE = 14;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //狀態
        resources.stateless(true)
                .accessDeniedHandler(authExceptionHandler)
                .authenticationEntryPoint(authExceptionHandler);
        //設定token儲存
        resources.tokenStore(redisTokenStore());
    }

    /**
     * 設定token儲存,這一點配置要與授權伺服器相一致
     */
    @Bean
    public RedisTokenStore redisTokenStore() {

        // 指定redis資料庫儲存token,與業務庫區分。
        LettuceConnectionFactory lettuceConnectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
        lettuceConnectionFactory.setDatabase(REDIS_CONNECTION_DATABASE);
        RedisTokenStore redisTokenStore = new RedisTokenStore(lettuceConnectionFactory);
        log.info("Oauth2 redis database : [{}]",lettuceConnectionFactory.getDatabase());

        //設定redis token儲存中的字首
        redisTokenStore.setPrefix("auth-token:");
        return redisTokenStore;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //請求許可權配置
        http.authorizeRequests()
                //下邊的路徑放行,不需要經過認證
                .antMatchers("/actuator/health").permitAll()
                .antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui",
                        "/swagger-resources", "/swagger-resources/configuration/security",
                        "/swagger-ui.html", "/webjars/**").permitAll()
                //其餘介面沒有角色限制,但需要經過認證,只要攜帶token就可以放行
                .antMatchers("/dictionary/**").permitAll()
                .anyRequest()
                .authenticated();
    }
}
AuthExceptionHandler   異常捕獲處理類
package com.adao.manage.common;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author adao
 * @version 1.0
 * @date 2021/8/11
 * @Description: 許可權不足返回資訊處理類
 */
@Component
@Slf4j
public class AuthExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        Throwable cause = authException.getCause();
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // CORS "pre-flight" request
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Cache-Control", "no-cache");
        response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
        response.addHeader("Access-Control-Max-Age", "1800");
        if (cause instanceof InvalidTokenException) {
            log.error("InvalidTokenException : {}", cause.getMessage());
            //Token無效
            response.getWriter().write(JSON.toJSONString(ApiResult.fail(ApiCode.ACCESS_TOKEN_INVALID)));
        } else {
            log.error("AuthenticationException : NoAuthentication");
            //資源未授權
            response.getWriter().write(JSON.toJSONString(ApiResult.fail(ApiCode.ACCESS_UNAUTHORIZED)));
        }
    }

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Cache-Control", "no-cache");
        response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
        response.addHeader("Access-Control-Max-Age", "1800");
        //訪問資源的使用者許可權不足
        log.error("AccessDeniedException : {}", accessDeniedException.getMessage());
        response.getWriter().write(JSON.toJSONString(ApiResult.fail(ApiCode.INSUFFICIENT_PERMISSIONS)));
    }
}

公用的範圍code及 含義

package com.adao.manage.common;


public enum ApiCode {

    SUCCESS(200, "操作成功"),

    /**
     * 表示介面呼叫方異常提示
     */
    ACCESS_TOKEN_INVALID(1001, "access_token 無效"),
    REFRESH_TOKEN_INVALID(1002, "refresh_token 無效"),
    INSUFFICIENT_PERMISSIONS(1003, "該使用者許可權不足以訪問該資源介面"),
    ACCESS_UNAUTHORIZED(1004, "訪問此資源需要身份驗證"),

   
}

最後開始測試

postman 設定

http://192.168.10.90:6005/oauth/token?grant_type=password&username=zq&password=123456&scope=all

直接結果可以看到,獲取到了token資料,這裡就結束了。