Spring Security OAuth2 Redis 模式下认证服务器

四种授权码模式

  • 授权码模式
  • 密码模式
  • 客户端模式
  • 简化模式

密码模式

  • grant_type:授权类型,必选,此处固定值“password”
  • username:表示用户名,必选
  • password:表示用户密码,必选
  • scope:权限范围,可选

(1)获取access_token

http://localhost:9003/oauth/token?username=user&password=password&grant_type=password&client_id=client&client_secret=secret
{
    "access_token":"9a263f17f894488fa0973136e6809198",
    "token_type":"bearer",
    "refresh_token":"1d9b97d9eeb74c62adf9f834868518e3",
    "expires_in":59,
    "scope":"all",
    "client_id":"client_password"
}

(2)通过refresh_token获取access_token

http://localhost:9003/oauth/token?grant_type=refresh_token&refresh_token=1d9b97d9eeb74c62adf9f834868518e3&client_id=client_password&client_secret=secret
{
    "access_token":"9229d299bf264b0d8771306d1da71f42",
    "token_type":"bearer",
    "refresh_token":"1d9b97d9eeb74c62adf9f834868518e3",
    "expires_in":59,
    "scope":"all",
    "client_id":"client_password"
}

授权码模式

  • client_id:客户端ID,必选
  • response_type:必须为code,必选
  • redirect_uri:回调url,必选

(1)获取授权码

http://localhost:9003/oauth/authorize?client_id=auth_code&response_type=code&redirect_uri=http://localhost:9003/auth_user/get_auth_code

(2)获取access_token

http://localhost:9003/oauth/token?grant_type=authorization_code&code=S4x5Yi&client_id=auth_code&client_secret=secret&redirect_uri=http://localhost:9003/auth_user/get_auth_code
{
    "access_token":"46d216c6e19c4761a95c3313883574d7",
    "token_type":"bearer",
    "refresh_token":"5124987a8e4643088de2ba03bdb24730",
    "expires_in":59,
    "scope":"select replace insert update del",
    "client_id":"auth_code"
}

若报错如下:

{"status":405,"message":"Request method 'GET' not supported"}

参考:https://github.com/lexburner/oauth2-demo/issues/3

@Configuration
public class OAuthSecurityConfig extends AuthorizationServerConfigurerAdapter {
...
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        ...
        endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //add get method
        ...

        endpoints.tokenServices(tokenServices);
    }
...
}

(3)通过refresh_token获取access_token

http://localhost:9003/oauth/token?grant_type=refresh_token&refresh_token=5124987a8e4643088de2ba03bdb24730&client_id=auth_code&client_secret=secret
{
    "access_token":"c90e2b29cc2e4b9192e68566148db8cf",
    "token_type":"bearer",
    "refresh_token":"604baf02b5294daa951fea6fede6a398",
    "expires_in":59,
    "scope":"select replace insert update del",
    "client_id":"auth_code"
}
D:\Server\Redis-x64-3.2.100>redis-cli.exe
127.0.0.1:6379> select 0
OK
127.0.0.1:6379> keys *
 1) "refresh:604baf02b5294daa951fea6fede6a398"
 2) "access_to_refresh:c90e2b29cc2e4b9192e68566148db8cf"
 3) "auth_to_access:a724bdfe1be1f3d30a3c89764c2adca9"
 4) "refresh_auth:604baf02b5294daa951fea6fede6a398"
 5) "refresh_to_access:604baf02b5294daa951fea6fede6a398"
 6) "codeCache"
 7) "client_id_to_access:auth_code"
 8) "uname_to_access:auth_code:admin"
 9) "auth:c90e2b29cc2e4b9192e68566148db8cf"
10) "access:c90e2b29cc2e4b9192e68566148db8cf"
11) "test"
127.0.0.1:6379>

(4)通过refresh_token获取新的access_token时可以自定义用户信息验证service

http://localhost:9003/auth_user/get_token_info?access_token=c90e2b29cc2e4b9192e68566148db8cf
{
    "authorities":[
        {
            "authority":"{"interfaces":["/a/b","/a/c","/oauth/token"]}"
        },
        {
            "authority":"{"username":"admin"}"
        }
    ],
    "details":{
        "remoteAddress":"0:0:0:0:0:0:0:1",
        "sessionId":"8233149F60205EF21BAD74B80BFE7F06"
    },
    "authenticated":true,
    "principal":{
        "password":null,
        "username":"admin",
        "authorities":[
            {
                "authority":"{"interfaces":["/a/b","/a/c","/oauth/token"]}"
            },
            {
                "authority":"{"username":"admin"}"
            }
        ],
        "accountNonExpired":true,
        "accountNonLocked":true,
        "credentialsNonExpired":true,
        "enabled":true
    },
    "credentials":null,
    "name":"admin"
}

Client模式

  • client_id: 客户端ID,必选
  • client_secret: 客户端密码,必选
  • grant_type: 必须为password,必选
  • scope: 授权范围,必选
http://localhost:9003/oauth/token?grant_type=client_credentials&scope=insert&client_id=client&client_secret=secret
{
    "access_token":"a1016035f36f493391648bb57b1bf6a7",
    "token_type":"bearer",
    "expires_in":59,
    "scope":"insert",
    "client_id":"client"
}

极简模式

http://localhost:9003/oauth/authorize?client_id=client_implicit&response_type=token&redirect_uri=http://localhost:9003/auth_user/get_token_info
{
    "authorities":[
        {
            "authority":"{"interfaces":["/a/b","/a/c","/oauth/token"]}"
        },
        {
            "authority":"{"username":"admin"}"
        }
    ],
    "details":{
        "remoteAddress":"0:0:0:0:0:0:0:1",
        "sessionId":"9B935C3B239869DF4A840B57F512F2E7"
    },
    "authenticated":true,
    "principal":{
        "password":null,
        "username":"admin",
        "authorities":[
            {
                "authority":"{"interfaces":["/a/b","/a/c","/oauth/token"]}"
            },
            {
                "authority":"{"username":"admin"}"
            }
        ],
        "accountNonExpired":true,
        "accountNonLocked":true,
        "credentialsNonExpired":true,
        "enabled":true
    },
    "credentials":null,
    "name":"admin"
}

用户认证

/**
 * @Description: 用户认证
 * @Package: cn.appblog.security.oauth2.service.UserAuthDetailsService
 * @Version: 1.0
 */
@Component
public class UserAuthDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 根据用户名查询用户角色、权限等信息
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        GrantedAuthority authority = new UserGrantedAuthority("username", username);

        JSONArray array = new JSONArray();
        array.add("/a/b");
        array.add("/a/c");
        array.add("/oauth/token");
        GrantedAuthority interfaces = new UserGrantedAuthority("interfaces", array);

        /**
         isEnabled 账户是否启用
         isAccountNonExpired 账户没有过期
         isCredentialsNonExpired 身份认证是否是有效的
         isAccountNonLocked 账户没有被锁定
         */
        return new User(username, passwordEncoder.encode("123456"),
                true,
                true,
                true,
                true,
                Arrays.asList(authority, interfaces));
    }

}

OAuth2授权服务配置

在Security OAuth2授权服务配置类中添加上自定义的用户信息校验类

/**
 * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    //token持久化容器
    tokenServices.setTokenStore(tokenStore);
    //客户端信息
    tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    //自定义token生成
    tokenServices.setTokenEnhancer(tokenEnhancer());
    //access_token 的有效时长 (秒), 默认 12 小时
    tokenServices.setAccessTokenValiditySeconds(60 * 1);
    //refresh_token 的有效时长 (秒), 默认 30 天
    tokenServices.setRefreshTokenValiditySeconds(60 * 2);
    //是否支持refresh_token,默认false
    tokenServices.setSupportRefreshToken(true);
    //是否复用refresh_token,默认为true(如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token)
    tokenServices.setReuseRefreshToken(false);

    endpoints
            .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) // add get method
            //通过authenticationManager开启密码授权
            .authenticationManager(authenticationManager)
            //自定义refresh_token刷新令牌对用户信息的检查,以确保用户信息仍然有效
            .userDetailsService(userAuthDetailsService)
            //token相关服务
            .tokenServices(tokenServices)
            .pathMapping("/oauth/confirm_access", "/custom/confirm_access")
            //自定义异常转换处理类
            .exceptionTranslator(webResponseExceptionTranslator);
}

自定义token生成

  • 自定义一个实现TokenEnhancer接口的token增强器
**
 * @Description: 用户自定义token令牌,包括access_token和refresh_token
 * @Package: cn.appblog.security.oauth2.enhancer.UserTokenEnhancer
 * @Version: 1.0
 */
public class UserTokenEnhancer implements TokenEnhancer {
    /**
     * 重新定义令牌token
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        if (accessToken instanceof DefaultOAuth2AccessToken) {
            DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
            token.setValue(getToken());
            //使用DefaultExpiringOAuth2RefreshToken类生成refresh_token,自带过期时间,否则不生效,refresh_token一直有效
            DefaultExpiringOAuth2RefreshToken refreshToken = (DefaultExpiringOAuth2RefreshToken) token.getRefreshToken();
            //OAuth2RefreshToken refreshToken = token.getRefreshToken();
            if (refreshToken instanceof DefaultExpiringOAuth2RefreshToken) {
                token.setRefreshToken(new DefaultExpiringOAuth2RefreshToken(getToken(), refreshToken.getExpiration()));
            }
            Map<String, Object> additionalInformation = Maps.newHashMap();
            additionalInformation.put("client_id", authentication.getOAuth2Request().getClientId());
            //添加额外配置信息
            token.setAdditionalInformation(additionalInformation);
            return token;
        }
        return accessToken;
    }

    /**
     * 生成自定义token
     */
    private String getToken() {
        return StringUtils.join(UUID.randomUUID().toString().replace("-", ""));
    }
}
  • 将自定义的token增强器加入IOC容器中
/**
 * 自定义生成令牌token
 */
@Bean
public TokenEnhancer tokenEnhancer() {
    return new UserTokenEnhancer();
}
  • 将token增强器加入授权配置端点
/**
 * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    ...
    tokenServices.setTokenEnhancer(tokenEnhancer());
    ...
}

自定义token过期时长

/**
 * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    //token持久化容器
    tokenServices.setTokenStore(tokenStore);
    //客户端信息
    tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
    //自定义token生成
    tokenServices.setTokenEnhancer(tokenEnhancer());
    //access_token 的有效时长 (秒), 默认 12 小时
    tokenServices.setAccessTokenValiditySeconds(60 * 1);
    //refresh_token 的有效时长 (秒), 默认 30 天
    tokenServices.setRefreshTokenValiditySeconds(60 * 2);
    //是否支持refresh_token,默认false
    tokenServices.setSupportRefreshToken(true);
    //是否复用refresh_token, 默认为true(如果为false, 则每次请求刷新都会删除旧的refresh_token, 创建新的refresh_token)
    tokenServices.setReuseRefreshToken(false);

    ...
}

认证服务器配置 - token存入redis缓存

  • 使用Redis缓存需要引入的依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 认证服务器配置代码
/**
 * @Description: @EnableAuthorizationServer注解开启OAuth2授权服务机制, 优先级顺序order=0
 * @Package: cn.appblog.security.oauth2.config.OAuth2ServerConfig
 * @Version: 1.0
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigure extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private OAuth2ClientDetailsService oAuth2ClientDetailsService;
    @Autowired
    private UserAuthDetailsService userAuthDetailsService;
    @Autowired
    private WebResponseExceptionTranslator webResponseExceptionTranslator;
    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private OAuthTokenAuthenticationFilter oAuthTokenAuthenticationFilter;

    /**
     * 用来配置客户端详情服务(ClientDetailsService),客户端详情信息在这里初始化,
     * 你可以把客户端详情信息写死也可以写入内存或者数据库中
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //使用自定义ClientDetailsService初始化配置
        clients.withClientDetails(oAuth2ClientDetailsService);
    }

    /**
     * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        //token持久化容器
        tokenServices.setTokenStore(tokenStore);
        //客户端信息
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        //自定义token生成
        tokenServices.setTokenEnhancer(tokenEnhancer());
        //access_token 的有效时长 (秒), 默认 12 小时
        tokenServices.setAccessTokenValiditySeconds(60 * 1);
        //refresh_token 的有效时长 (秒), 默认 30 天
        tokenServices.setRefreshTokenValiditySeconds(60 * 2);
        //是否支持refresh_token,默认false
        tokenServices.setSupportRefreshToken(true);
        //是否复用refresh_token,默认为true(如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token)
        tokenServices.setReuseRefreshToken(false);

        endpoints
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST) // add get method
                //通过authenticationManager开启密码授权
                .authenticationManager(authenticationManager)
                //自定义refresh_token刷新令牌对用户信息的检查,以确保用户信息仍然有效
                .userDetailsService(userAuthDetailsService)
                //token相关服务
                .tokenServices(tokenServices)
                /**
                 pathMapping用来配置端点URL链接,第一个参数是端点URL默认地址,第二个参数是你要替换的URL地址
                 上面的参数都是以“/”开头,框架的URL链接如下:
                 /oauth/authorize:授权端点。对应的类:AuthorizationEndpoint.java
                 /oauth/token:令牌端点。对应的类:TokenEndpoint.java
                 /oauth/confirm_access:用户确认授权提交端点。对应的类:WhitelabelApprovalEndpoint.java
                 /oauth/error:授权服务错误信息端点
                 /oauth/check_token:用于资源服务访问的令牌解析端点
                 /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话
                 */
                .pathMapping("/oauth/confirm_access", "/custom/confirm_access")
                //自定义异常转换处理类
                .exceptionTranslator(webResponseExceptionTranslator);
    }

    /**
     * 用来配置令牌端点(Token Endpoint)的安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("permitAll()")
                /**
                 * 主要是让/oauth/token支持client_id和client_secret做登陆认证
                 * 如果开启了allowFormAuthenticationForClients,那么就在BasicAuthenticationFilter之前
                 * 添加ClientCredentialsTokenEndpointFilter,使用ClientDetailsUserDetailsService来进行
                 * 登陆认证
                 */
                .allowFormAuthenticationForClients()
                //oauth/token端点过滤器
                .addTokenEndpointAuthenticationFilter(oAuthTokenAuthenticationFilter);
    }

    /**
     * ApprovalStore用户保存、检索和撤销用户审批的界面
     */
    /*
    @Bean
    public ApprovalStore approvalStore() throws Exception {
        TokenApprovalStore store = new TokenApprovalStore();
        store.setTokenStore(tokenStore());
        return store;
    }
    */

    /*
    @Bean
    public UserApprovalHandler userApprovalHandler1(){
        TokenStoreUserApprovalHandler userApprovalHandler = new TokenStoreUserApprovalHandler();
        userApprovalHandler.setTokenStore(tokenStore());
        return userApprovalHandler;
    }
    */

    /**
     * 自定义生成令牌token
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new UserTokenEnhancer();
    }

}
  • Redis缓存配置
##单机应用环境配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
#spring.redis.password=
##Redis数据库索引,默认0
spring.redis.database=0
#spring.redis.timeout=

##redis连接池配置
## 连接池中的最小空闲连接,默认0
spring.redis.jedis.pool.min-idle=0
## 连接池中的最大空闲连接,默认8
spring.redis.jedis.pool.max-idle=8
## 连接池最大阻塞等待时间(使用负值表示没有限制),默认-1ms
spring.redis.jedis.pool.max-wait=-1ms
##连接池最大连接数(使用负值表示没有限制),默认8
spring.redis.jedis.pool.max-active=8

基于Spring Security的安全认证

替换的核心是将InMemoryTokenStore对象更换为RedisTokenStore对象,并传递一个RedisConnectionFactory接口,接口的具体实现类是JedisConnectionFactory类;

RedisConnectionFactory可以通过如下三个配置类应用在不同的应用场景:

  • RedisStandaloneConfiguration:RedisConnectionFactory 工厂类单机模式的配置类
  • RedisSentinelConfiguration:RedisConnectionFactory 工厂类高可用模式的配置类
  • RedisClusterConfiguration:RedisConnectionFactory 工厂类集群模式的配置类
/**
 * @Description: 启动基于Spring Security的安全认证, 优先级顺序order=100
 * @Package: cn.appblog.security.oauth2.config.WebSecurityConfigurer
 * @Version: 1.0
 */
@Configuration
@EnableWebSecurity(debug = true)
@Order(2)
public class BaseSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Autowired
    private UserAuthDetailsService authUserDetailsService;
    @Autowired
    private UserAuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private UserAccessDeniedHandler accessDeniedHandler;
    @Autowired
    private UserLogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http
//                .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**", "/test/**")
//                .and()
//                .authorizeRequests()
//                .antMatchers("/oauth/**").authenticated()
//                .and()
//                .formLogin().loginPage("/test/login").permitAll(); //新增login form支持用户登录及授权
                .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**", "/test/**")
                .and()
                // 指定支持基于表单的身份验证, 如果未指定FormLoginConfigurer#loginPage(String), 则将生成默认登录页面
                .formLogin()
                // 自定义登录页url, 默认为/login
                .loginPage("/test/login")
                // 登录请求拦截的url, 也就是form表单提交时指定的action
                .loginProcessingUrl("/user/login")
                // 用户名的请求字段 username
                .usernameParameter("username")
                // 密码的请求字段 默认为password
                .passwordParameter("password")
                // 登录成功
                // .successHandler(authenticationSuccessHandler)
                // 登录失败
                .failureHandler(authenticationFailureHandler)
                // 无条件允许访问
                .permitAll()
                .and()
                .requestMatchers()
                .anyRequest()
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**")
                .authenticated()
                //.permitAll()
                /* .and()
                  // 其它的请求要求必须有身份认证
                  .authorizeRequests()
                  .anyRequest()
                  .authenticated()
                  */
                .and()
                .logout()
                .logoutSuccessHandler(logoutSuccessHandler)
                .permitAll()
                .and()
                // 认证过的用户访问无权限资源时的处理
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
        http.csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        //忽略swagger访问权限限制
        // web.ignoring().antMatchers(
        // "/userlogin",
        // "/userlogout",
        // "/userjwt",
        // "/v2/api-docs",
        // "/swagger-resources/configuration/ui",
        // "/swagger-resources",
        // "/swagger-resources/configuration/security",
        // "/swagger-ui.html",
        // "/css/**",
        // "/js/**",
        // "/images/**",
        // "/webjars/**",
        // "**/favicon.ico",
        // "/index");
        super.configure(web);
    }

    /**
     * Spring Security认证服务中的相关实现重新定义
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 加入自定义的安全认证
        auth.userDetailsService(this.authUserDetailsService)
                .passwordEncoder(this.passwordEncoder())
                .and()
                .authenticationProvider(smsAuthenticationProvider())
                .authenticationProvider(authenticationProvider());
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * Spring security认证Bean
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        AuthenticationProvider authenticationProvider = new UserAuthenticationProvider();
        return authenticationProvider;
    }

    @Bean
    public AuthenticationProvider smsAuthenticationProvider() {
        AuthenticationProvider authenticationProvider = new UserSmsAuthenticationProvider();
        return authenticationProvider;
    }

    /**
     * 自定义加密
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * OAuth2 token持久化接口
     */
    @Bean
    public TokenStore tokenStore() {
        //token保存在内存中(也可以保存在数据库、Redis中)
        //如果保存在中间件(数据库、Redis),那么资源服务器与认证服务器可以不在同一个工程中
        //注意:如果不保存access_token,则没法通过access_token取得用户信息
        //return new InMemoryTokenStore();
        return new RedisTokenStore(redisConnectionFactory);
    }
}

本文转载参考 原文 并加以调试

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/19/spring-security-oauth2-authentication-server-in-redis-mode/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Spring Security OAuth2 Redis 模式下认证服务器
四种授权码模式 授权码模式 密码模式 客户端模式 简化模式 密码模式 grant_type:授权类型,必选,此处固定值“password” username:表示用户名,必选 passw……
<<上一篇
下一篇>>
文章目录
关闭
目 录