本文是梳理整合SpringCloud和SpringSecurity OAuth2的搭建流程!好久没撸SpringSecurity OAuth2这系列代码了,都快忘了,特写此文章梳理脉络!开干!!!
Maven版本微服务版本
2.3.2.RELEASE Hoxton.SR9 2.2.5.RELEASE com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring-cloud-alibaba.version} pom import org.springframework.cloud spring-cloud-dependencies ${spring-cloud.version} pom import
SpringSecurity OAuth2版本
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
通过微服务版本限定后spring-security-oauth2-autoconfigure的最终版本自动适配为2.1.2
授权码模式刚开始我这里就不一次性把一大堆配置放上来,需要什么就写什么,不然到时候都搞不清那个配置是干嘛,有什么用的!这也是我写这个文章的缘由!
授权服核心配置-AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
SpringSecurity核心配置
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
启动服务器-访问测试
访问http://localhost:3000/oauth/authorize?response_type=code&client_id=tao&redirect_uri=http://baidu.com&scope=all
任意输入账号密码试试
这是因为我们啥也没配置!
配置密码加密
WebSecurityConfig中
@Bean
public PasswordEncoder passwordEncoder() {//密码加密
return new BCryptPasswordEncoder();
}
配置登录用户账号密码
WebSecurityConfig中
@Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder().encode("123456")).roles("USER","ADMIN").authorities(AuthorityUtils.commaSeparatedStringToAuthorityList("p1,p2"));
//这里配置全局用户信息
}
授权服配置端点信息
AuthorizationServerConfig
@Autowired
PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//基于内存便于测试
clients.inMemory()// 使用in-memory存储
.withClient("tao")// client_id
//.secret("secret")//未加密
.secret(passwordEncoder.encode("secret"))//加密
//.resourceIds("res1")//资源列表
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all", "ROLE_ADMIN", "ROLE_USER")// 允许的授权范围
//.autoApprove(false)//false跳转到授权页面
//加上验证回调地址
.redirectUris("http://baidu.com");
}
重启服务测试
http://localhost:3000/oauth/authorize?response_type=code&client_id=tao&redirect_uri=http://baidu.com&scope=all
登录成功得到授权码
授权码获取token
这里授权码就基本搞定了!接下来我们试试密码模式
Unsupported grant type: password,默认不支持密码模式,需要而外配置下!
配置认证管理器-AuthenticationManager
WebSecurityConfig中
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
授权服配置密码模式
AuthorizationServerConfig
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {//配置令牌的访问端点和令牌服务
endpoints
.authenticationManager(authenticationManager)//认证管理器
;
}
重启访问测试
成功!
这个就简单了,token直接是显示在地址栏上的http://localhost:3000/oauth/authorize?client_id=tao&response_type=token&scope=all&redirect_uri=http://baidu.com
客户端模式这里就不演示了,实际上用的并不多!那么到这里,授权基本上的就搞定了,至于其他配置下文会深入,这里我们既然得到了Token那么我们就可以测试一下认证!
创建测试资源
@Slf4j
@RestController
@RequestMapping("/mbb")
public class MbbController {
@GetMapping("/init")
public R init(){
return R.ok();
}
}
@Slf4j
@RestController
@RequestMapping("/oth")
public class OthController {
@GetMapping("/init")
public R init(){
return R.ok();
}
}
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/init")
public R init(){
return R.ok();
}
}
实际上也就是三个请求,/oth/init、/test/init、/test/init
认证测试
我们通过密码模式做授权得到Token
访问测试/oth/init
同样访问其他的也是一样!思考下是什么问题?刚开始检查了下代码以为是WebSecurityConfig中没有配置安全策略,那么我们配置一下!
配置安全策略
WebSecurityConfig中
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()//系统中所有请求
.antMatchers("
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")//开放获取tokenkey(这个是做JWT的时候开放的!也就是资源服可以通过这个请求得到JWT加密的key,目前这里没什么用,可以不用配置)
.checkTokenAccess("permitAll()")//开放远程token检查 /oauth/check_token body token
.allowFormAuthenticationForClients();//允许client使用form的方式进行authentication的授权
}
其他配置不动!重启服务器测试!
通过密码模式得到Token
这里请求只被资源服接管,所以这里的请求都是符合预期的,这里如果我们有成千上百台资源服,那么每个请求都要远程调用授权服进行认证,那么我们的授权服压力会很大,所以我们可以使用上面代码中提到的JWT,下面我们就开启配置JWT颁发Token!
在搞JWT之前我建议先了解下TokenStore,在了解TokenStore的时候呢顺便有可以了解下AuthorizationCodeServices
AuthorizationCodeServicesSpringSecurity OAuth2关于AuthorizationCodeServices
SpringSecurity OAuth2关于TokenStore
SpringSecurity OAuth使用JWT替换默认Token
SpringSecurityOAuth2采用JWT生成Token的模式自定义JWT数据
获取JWT中的数据SpringSecurityOAuth2获取JWT中的数据
那么到这里,我们对token的生成,存储策略有了进一步的了解,那么现在我们的授权服是使用JWT生成Token,同时自定义了一些数据,然后Redis中也存储一份,虽然JWT是自包含,且可以设置过期时间,但是这个是为了满足实际业务,例如,当我们给用户禁用后,然后之前颁发的JWT格式的Token还是能解析出数据,那么这里就不符合业务逻辑,所以这里存储一份在Redis中是为了当我们将用户禁用后,把Redis中的Token也删除,这样用户禁用后携带的老JWT格式Token再到Redis中对比就对比失败,那么这样就能及时更新用户状态信息!接下来我们采用现在的代码,测试一下资源服鉴权
资源服鉴权JWT格式Token因为我们采用 是JWT格式的Token,那么这个Token中是携带权限信息的,只要配置好资源服,JWT的Token格式资源服已经帮我们实现好了解析!我们按在上面的文章,将JWT格式Token在授权服中配置好后,授权服就不用动了,既然Token中携带了权限信息那么也就意味着资源服无需远程访问授权服进行Token检查,我们改造下资源服核心配置!如下!
改造资源服核心配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "res1";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)//资源 id
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()//授权的请求
.anyRequest()//任何请求
.authenticated()//需要身份认证
.and().csrf().disable();
}
}
注意这里有一个tokenStore注入,那么此时需要和授权服保持一致,授权服采用的是Redis那么这里也需要采用Redis,那么这时资源服就知道从哪读取Token然后通过Redis中得到的Token解析对比,所以这个tokenStore至关重要!,通常我们会将tokenStore抽离出来,让资源服和授权服共用同一个tokenStore,那么这样就可以保证tokenStore的存取方式是一致的!
TokenStore
@Slf4j
@Configuration
public class TokenStoreConfig {
String OAUTH_ACCESS = "yy:access:";
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
@ConditionalOnProperty(prefix = "security.oauth2", name = "tokenStore", havingValue = "memory" ,matchIfMissing=true)
public TokenStore tokenStoreInMemory() {
log.info("===>"+"tokenStoreInMemory");
return new InMemoryTokenStore();
}
@Bean
@ConditionalOnProperty(prefix = "security.oauth2", name = "tokenStore", havingValue = "redis")
public TokenStore tokenStoreInRedis() {
log.info("===>"+"tokenStoreInRedis");
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(OAUTH_ACCESS);//设置Redis中OAuth相关前缀
return tokenStore;
}
}
我这里的是将授权服和资源服分了模块,让将整个SpringSecurity OAuth2共用的配置写到common中,然后授权服和资源服去引用common中的配置,这样后期使用起来非常方便,只需要创建普通额Spring Web项目,需要做授权服的就直接引用授权服依赖即可,需要做资源服额就直接引用资源服额依赖即可,这样保证我们无需开发冗余代码,而且这样使得架构复用性提高!
重启服务器测试资源服检验JWT格式Token
资源权限还是不变
/mbb/init还是需要p8权限、/oth/init还是需要p1、/test/init不设置权限,但是需要Token,我们的用户默认还是携带p1、p2权限,那么测试开始!
测试成功!那么到这里独立资源服鉴权JWT格式Token就完成了,那么上面既然提到JWT格式的Token是自包含,那么我们就解析JWT中的数据,这里还有一个问题,就是这个认证失败的返回,这里/mbb/init权限不够,返回的信息是SpringSecurity OAuth2默认的信息,不是很准确,对前端、用户不是很友好,那么这个将会在后面进行改造!归结为SpringSecurity OAuth2异常处理!
过去写过一篇这个文章,介绍很详细,这里就不徒劳了,SpringSecurityOAuth2获取JWT中的数据
由于SpringSecurity OAuth2异常处理这部分篇幅较长,单独拎出来写篇文章!SpringSecurity OAuth2异常处理OAuth2Exception
SpringSecurity OAuth2自定义授权模式(短信验证码授权)之前写过一篇这个文章,在我写这篇文章的时候也是按照那篇文章复制粘贴过来的,直接可以用!SpringSecurityOauth2自定义授权模式
这篇文章还有一些相关联的,大家可以参看阅读一下!
SpringSecurityOAuth2授权流程源码分析(自定义验证码模式)
SpringSecurityOAuth2授权流程加载源码分析
SpringSecurityOAuth2授权流程源码分析
文章到这里那么就还剩打通数据库的操作了,本文之前都是没有打通数据库的,端点信息是写死在代码中的,然后用户信息也是写死在代码中的,那么接下来就完成主流程的最后一步,连接数据库!
持久化数据库这里持久化数据库有两部分数据,一部分是端点数据,一部分是用户数据,我这里只写端点数据配置读取数据库,因为用户数据这块之前写过,这里也提一嘴,为什么我愿意在SpringSecurity这个框架上花这么多时间,其实是因为我写的第一篇博客其实就是关于SpringSecurity的,所以,从Springboot+Mybatis+Springsecurity+MySQL的整合这篇文章开始,到Spring Cloud整合SpringSecurity OAuth2(全网最强)这篇文章,我从只会简单接入SpringSecurity,到深入了解整个体系,深入源码,一路下来有点爽!哈哈哈
!
用户数据部分:
Springboot+Mybatis+Springsecurity+MySQL的整合
SpringSecurity从数据库获取用户信息
端点数据部分
这个其实特别简单了就直接贴代码了!前提是想导入数据库数!
@Autowired(required = false)
private DataSource dataSource;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
log.info("===>"+"基于数据库");
clients.withClientDetails(new JdbcClientDetailsService(dataSource));
}
这种方式只能使用默认的表名,如果我们想修改表名也是可以的换写为下面的方案!
@Autowired(required = false)
private DataSource dataSource;
String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "
+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "
+ "refresh_token_validity, additional_information, autoapprove";
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
log.info("===>"+"基于数据库");
String selectClientDetailsSql = "select "+ CLIENT_FIELDS + " from 自定义表名where client_id = ?";
String findClientDetailsSql ="select " + CLIENT_FIELDS + " from 自定义表名order by client_id ";
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
jdbcClientDetailsService.setSelectClientDetailsSql(selectClientDetailsSql);
jdbcClientDetailsService.setFindClientDetailsSql(findClientDetailsSql);
clients.withClientDetails(jdbcClientDetailsService);
}
这里还提供一种方案,就是我们可以将断点信息存放到缓存中,实际业务中端点信息其实便更并不频繁!那么我们可以使用如下方案!我们编写一个自己的ClientDetailsService继承JdbcClientDetailsService然后重写loadClientByClientId方法即可
创建CacheClientDetailsService
public class CacheClientDetailsServiceextends JdbcClientDetailsService {
public CacheClientDetailsService(DataSource dataSource) {
super(dataSource);
}
@Override
@SneakyThrows
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
public ClientDetails loadClientByClientId(String clientId) {
return super.loadClientByClientId(clientId);
}
}
在改造一下configure中的
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
更改为
CacheClientDetailsServiceextends jdbcClientDetailsService = new CacheClientDetailsServiceextends (dataSource);
即可!
那么至此主流程将全部完成,后续会出一篇我关于SpringSecurity 和SpringSecurity OAuth2 的文章合集!



