-
前言
談到微服務一定會聯想到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
項目后續繼續更新,歡迎大佬來吐槽