前言
谈到微服务一定会联想到spring-cloud。微服务将服务拆分,这个时候想要做安全管理就比较难了。为此spring提供了spring-cloud-security模块,专门解决这部分问题。spring-cloud-security可以实现单点登录,也可以实现基于oauth协议的认证方式。
oauth应该不会陌生,对接过微信登录,qq登录的都知道,他们就是基于oauth实现。这是一个安全协议,第三方不会接触到用户的敏感信息,例如用户名,密码等。至于oauth详细介绍可以查阅官方文档,也可以查看这博客(点击这里查看博客)
本篇博客主要是记录springboot+springcloud+springsecurity+vue的基本实现方式
项目架构
首先看一下项目的结构:
- 认证服务
- 资源服务
- 用户服务
- 用户客户端
其中资源服务同时也是网关(Zuul)
这里的注册中心选用的阿里巴巴的nacos,前后端分离必然会出现跨域问题这里选用nginx处理,token信息保存在redis中,客户端配置和资源配置基于数据库(mysql),服务调用使用feign
认证服务配置
首先配置SecurityConfig,在类上添加注解@Order(1),这样能保证SpringSecurity配置先于认证配置
首先配置两个基于内存的用户以便测试
以上两个配置都是基本spring-security的基本配置,不多说,下边介绍认证服务的配置
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private RedisConnectionFactory redisConnectionFactory;
// @Resource
// private ClientDetailsService clientDetailsService;
@Resource
private DataSource dataSource;
@Resource(name = "clientDetailsServiceImpl")
private ClientDetailsService clientDetailsService;
/**
* 安全配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()")//验证token放开权限
.allowFormAuthenticationForClients();//开启表单登录
}
/**
* 客户端配置
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//配置客户端
clients.withClientDetails(clientDetailsService).build();
// clients.inMemory()
// .withClient("system")
// .resourceIds("sys")
// .secret(passwordEncoder.encode("123"))
// .accessTokenValiditySeconds(3000)
// .authorizedGrantTypes("authorization_code", "refresh_token", "password")
// .redirectUris("http://localhost:1103/api/test/lhf/hello", "https://oauth.pstmn.io/v1/callback")
// .scopes("all")
// .autoApprove(true);
}
/**
* 节点配置
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authorizationCodeServices(authorizationCodeServices())
// .tokenServices(tokenServices())
;
}
@Bean
public TokenStore tokenStore() {
//配置token保存策略
return new RedisTokenStore(redisConnectionFactory);
}
// @Bean
// public AuthorizationServerTokenServices tokenServices() {
// DefaultTokenServices tokenServices = new DefaultTokenServices();
// tokenServices.setTokenStore(tokenStore());
// tokenServices.setClientDetailsService(clientDetailsService);
// tokenServices.setRefreshTokenValiditySeconds(60 * 30);
// tokenServices.setReuseRefreshToken(true);
// tokenServices.setSupportRefreshToken(true);
// return tokenServices;
// }
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
// return new InMemoryAuthorizationCodeServices();//配置授权码
return new JdbcAuthorizationCodeServices(dataSource);
}
这里是基于数据库管理客户端信息我这里提供一份基本的数据脚本:
DROP TABLE IF EXISTS `client_details`;
CREATE TABLE `client_details` (
`client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客户端id',
`client_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端名字',
`secret_required` int(11) NOT NULL DEFAULT 1 COMMENT '密码是否是必须的 0 不是,1 是',
`client_secret` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '客户端密码',
`scoped` int(11) NULL DEFAULT 1 COMMENT '是否有授权范围 0 无 1有',
`access_token_validity_seconds` int(11) NULL DEFAULT 2592000 COMMENT 'token有效期',
`auto_approve` int(11) NOT NULL DEFAULT 0 COMMENT '是否自动授权',
`refresh_token_validity_seconds` int(11) NOT NULL DEFAULT 18000,
UNIQUE INDEX `client_details_client_id_uindex`(`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客户端信息表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `resource_details`;
CREATE TABLE `resource_details` (
`resource_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '资源id',
`resource_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '资源名',
`client_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '客户端id',
`scope` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '作用域',
`authorized_grant_type` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '授权类型',
`registered_redirect_uri` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '重定向地址',
`authority` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限',
PRIMARY KEY (`resource_id`) USING BTREE,
INDEX `resource_details_client_details_client_id_fk`(`client_id`) USING BTREE,
CONSTRAINT `resource_details_client_details_client_id_fk` FOREIGN KEY (`client_id`) REFERENCES `client_details` (`client_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '资源表' ROW_FORMAT = Dynamic;
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`authentication` blob NULL
) ENGINE = MyISAM CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
按照顺序执行以上sql即可,前两个不用解释,第三个表是干嘛的?因为我接下来配置的将是基于授权码的认证服务这个表是保存授权码的(其实放到内存就行,这个东西只用一次,持久化没什么意义,当然认证服务要部署集群的话持久化还是有必要的)
接下来就是ClientDetailsService配置,
spring提供了两个实现,一个是基于内存实现,一个是基于数据库实现,我这里虽然是基于数据库实现但是并没有使用内置提供的,而是自己实现的。
可以看到,这里的东西是和springsecurity的用户部分是一样的,用实现的UserDetails和UserDetailsService,这个是实现ClientDetails和ClientDetailsService两个接口。这里具体实现就不多少了,文章最后有项目地址。
资源服务器配置
资源服务顾名思义管理资源的服务,授权也将是他来提供:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("8e9daef0-5494-48da-8f77-22ac923828bf")
.authenticationEntryPoint(new OauthAuthenticationEntryPoint())//未登录时的返回
.accessDeniedHandler(new OauthAccessDeniedHandler())//没有权限的返回
.tokenServices(tokenServices());
}
@Override
public void configure(HttpSecurity http) throws Exception {
// 安全配置,这里可以配置权限信息了
http.authorizeRequests()
// .antMatchers("/api/auth/oauth/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf()
.disable()
.cors();
}
@Bean
public RemoteTokenServices tokenServices() {
// 验证token,以及客户端配置
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("e5170418-8560-460b-9296-d7bd95a06a5e");
tokenServices.setClientSecret("123");
tokenServices.setCheckTokenEndpointUrl("http://localhost:8001/oauth/check_token");
return tokenServices;
}
}
资源服务也就这么多的配置啦,记录一下vue部分的吧,这里将是我遇到最大的坑
vue部分
理论上来讲授权过程是我们访问http://ip:port/oauth/authorize?client_id=xxxxxx&response_type=xxx&scope=xxx&redirect_uri=xxx,授权服务自动跳转登录页面,登录成功后授权服务器将会跳转到配置的重定向地址中并且携带code,即授权码,然后在访问/oauth/token即可,而且刚开始使用postman测试都好好的,但是一到vue上就一直在访问/oauth/token的时候提示单站不安全,需要登录,但是我明明登录了,而且授权服务都下发授权码了,头都快大了。查阅了很多资料一无所获,最终我用postman请求跟了几次代码才发现,在请求/oauth/token路径的时候也是要验证客户端的,但是这个时候我并没有将可客户端信息传过去所以授权服务认为是未登录。那就要想个办法传过去呀,我发送请求是通过axios发送的ajax请求,我尝试了data传参,也尝试了params传参,统统无果。后来我发现我直接访问这个路径是没用的,查看官网再找原来是这样请求的http://client:search@ip:port/oauth/token.其中Client:就是客户端id,search就是对应的密码!接下来就不扯皮了,上代码吧
首先是请求认证将会跳转登录页面
输入用户登录信息登录后返回设置的重定向地址并携带code
可以看到已经返回code,这是我们需要做的额是将这个code或取出来,
这里通过window.location.search获取请求参数并且截取,然后调用getToken方法
请求token的是一定是token方法,请求头一定有'Content-Type': 'application/x-www-form-urlencoded',否则是不可以的,当然在请求的时候不单单要传入code,还要将grant_type,redirect_uri一并传入,否则会报错
请求成功后将信息保存到cookie中,后边请求使用。
至于后边的toIndex方法就是跳转回首页
项目地址:https://github.com/Liuhuifa/FVS.git
前端地址: https://github.com/Liuhuifa/fvs-vue.git
项目后续继续更新,欢迎大佬来吐槽