該篇文章主要記錄,使用spring cloud security oauth2 的一些過程。
關於spring cloud security oauth2一些基本知識參考: Spring Security OAuth 2開發者指南;
編寫過程過也參考過其餘文章教程,例如:Spring cloud微服務實戰——基於OAUTH2.0統一認證受權的微服務基礎架構;Spring Boot Security OAuth2 例子(Bcrypt Encoder) ;Spring Security OAuth2實現使用JWT。html
Maven配置
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>com.hq</groupId> <artifactId>common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.hq</groupId> <artifactId>cloud-feign-client</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <nonFilteredFileExtensions>cert</nonFilteredFileExtensions> <nonFilteredFileExtensions>jks</nonFilteredFileExtensions> </configuration> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
安全配置
主要繼承 WebSecurityConfigurerAdapter 實現訪問資源以前的攔截配置。該攔截器的順序在資源服務器攔截器以前。
代碼以下:java
package com.hq.biz.config; import com.hq.biz.service.CustomUserDetailsService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * @author hq * @Package com.hq.cloud.oauth2server.config * @Description: sercurity安全配置 * @date 2018/4/13 11:44 */ @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; /** * 攔截配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //關閉csrf,攔截全部請求 http.csrf().disable() .authorizeRequests() .antMatchers("/oauth/remove_token").permitAll() .anyRequest().authenticated(); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring() .antMatchers("/favor.ico"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //替換成本身驗證規則 //auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder()); auth.authenticationProvider(authenticationProvider()); } /** * password 驗證須要設置 */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public static BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(customUserDetailsService); provider.setPasswordEncoder(passwordEncoder()); provider.setHideUserNotFoundExceptions(false); return provider; } }
資源服務器
資源服務器攔截除排除自定義刪除token的地址,以及替換成自定義的錯誤返回。
package com.hq.biz.config; import com.hq.biz.handler.CustomAccessDeniedHandler; import com.hq.biz.handler.CustomAuthEntryPoint; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; 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; /** * @author hq * @Package com.hq.biz.config * @Description: ResourceServerConfig * @date 2018/6/27 9:39 */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { Logger log = LoggerFactory.getLogger(ResourceServerConfig.class); @Autowired private CustomAuthEntryPoint customAuthEntryPoint; @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Override public void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(customAuthEntryPoint) .and().authorizeRequests() .antMatchers("/oauth/remove_token").permitAll() .anyRequest().authenticated(); ; } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); resources.authenticationEntryPoint(customAuthEntryPoint).accessDeniedHandler(customAccessDeniedHandler); } }
認證服務器
我採用jdbc 數據庫的方式存儲客戶端的信息;採用redis存儲令牌信息。
JDBC 使用的是mysql,表結構以下:mysql
CREATE TABLE `oauth_client_details` ( `client_id` varchar(128) NOT NULL, `resource_ids` varchar(128) DEFAULT NULL, `client_secret` varchar(128) DEFAULT NULL, `scope` varchar(128) DEFAULT NULL, `authorized_grant_types` varchar(128) DEFAULT NULL, `web_server_redirect_uri` varchar(128) DEFAULT NULL, `authorities` varchar(128) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(128) DEFAULT NULL, PRIMARY KEY (`client_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT; insert into `oauth_client_details` (`client_id`, `resource_ids`, `client_secret`, `scope`, `authorized_grant_types`, `web_server_redirect_uri`, `authorities`, `access_token_validity`, `refresh_token_validity`, `additional_information`, `autoapprove`) values('hq',NULL,'$2a$10$.8enc0qC92YpTnS7GR8MCO2yF33AGRRgpHtyshN48Os2gPLWQ4Sri','xx','password,refresh_token',NULL,NULL,NULL,NULL,NULL,NULL);
配置以下:web
package com.hq.biz.config; import com.hq.biz.converter.CustJwtAccessTokenConverter; import com.hq.biz.handler.CustomAccessDeniedHandler; import com.hq.biz.handler.CustomAuthEntryPoint; import com.hq.biz.handler.CustomWebResponseExceptionTranslator; import com.hq.biz.service.CustomUserDetailsService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.authentication.AuthenticationManager; 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.ClientDetailsService; import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; import javax.sql.DataSource; import java.util.concurrent.TimeUnit; /** * @author hq * @Package com.hq.biz.cloud.oauth2server.config * @Description: 認證服務器 * @date 2018/4/12 15:16 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { private Logger log = LoggerFactory.getLogger(AuthorizationServerConfig.class); @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private CustomWebResponseExceptionTranslator customWebResponseExceptionTranslator; @Autowired private CustomAuthEntryPoint customAuthEntryPoint; @Autowired private CustomAccessDeniedHandler customAccessDeniedHandler; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.allowFormAuthenticationForClients() .checkTokenAccess("isAuthenticated()") .tokenKeyAccess("permitAll()") .authenticationEntryPoint(customAuthEntryPoint) .accessDeniedHandler(customAccessDeniedHandler); log.info("AuthorizationServerSecurityConfigurer is complete"); } /** * 配置客戶端詳情信息(Client Details) * clientId:(必須的)用來標識客戶的Id。 * secret:(須要值得信任的客戶端)客戶端安全碼,若是有的話。 * scope:用來限制客戶端的訪問範圍,若是爲空(默認)的話,那麼客戶端擁有所有的訪問範圍。 * authorizedGrantTypes:此客戶端能夠使用的受權類型,默認爲空。 * authorities:此客戶端能夠使用的權限(基於Spring Security authorities)。 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetails()); log.info("ClientDetailsServiceConfigurer is complete!"); } /** * 配置受權、令牌的訪問端點和令牌服務 * tokenStore:採用redis儲存 * authenticationManager:身份認證管理器, 用於"password"受權模式 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .userDetailsService(customUserDetailsService) .tokenServices(tokenServices()) .exceptionTranslator(customWebResponseExceptionTranslator); log.info("AuthorizationServerEndpointsConfigurer is complete."); } /** * redis存儲方式 * * @return */ @Bean("redisTokenStore") public TokenStore redisTokenStore() { return new RedisTokenStore(redisConnectionFactory); } /* @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtTokenEnhancer()); }*/ /** * 客戶端信息配置在數據庫 * * @return */ @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } /** * 採用RSA加密生成jwt * * @return */ @Bean public JwtAccessTokenConverter jwtTokenEnhancer() { KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("hq-jwt.jks"), "hq940313".toCharArray()); /* JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("hq-jwt"));*/ CustJwtAccessTokenConverter tokenConverter = new CustJwtAccessTokenConverter(); tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("hq-jwt")); return tokenConverter; } /** * 配置生成token的有效期以及存儲方式(此處用的redis) * * @return */ @Bean public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(redisTokenStore()); defaultTokenServices.setTokenEnhancer(jwtTokenEnhancer()); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(30)); defaultTokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); return defaultTokenServices; } }
自定義UserDetailsService校驗
這邊我只是作個簡單的判斷。redis
@Service public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { UserDetail userDetail = new UserDetail(); if ("huang".equals(s)) { Permission permission = new Permission(); permission.setPerCode("user:edit"); List<Permission> plist = new ArrayList<>(); plist.add(permission); Role role = new Role(); role.setRoleCode("admin"); role.setPermissions(plist); List<Role> roleList = new ArrayList<>(); roleList.add(role); userDetail.setRoles(roleList); userDetail.setUserName(s); userDetail.setPassWord(BCryptUtil.encode("123456")); } else { throw new UsernameNotFoundException(s); } return userDetail; } }
用戶信息查看以及token刪除
package com.hq.biz.controller; import com.hq.biz.entity.Result; import com.hq.biz.enums.ResultEnum; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; /** * @author Administrator * @Package com.hq.cloud.oauth2server.controller * @Description: 返回用戶信息 * @date 2018/4/13 13:58 */ @RestController public class UserController { @Autowired @Qualifier("redisTokenStore") private TokenStore tokenStore; @GetMapping("/user") public Principal user(Principal user) { return user; } @RequestMapping("/oauth/remove_token") public Result removeToken(@RequestParam("token") String token) { if (token != null) { OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); tokenStore.removeAccessToken(accessToken); } else { return new Result(ResultEnum.TOKEN_MISS); } return Result.returnOk(); } }
配置文件application.yml
server: port: 8000 spring: application: name: cloud-oauth2 redis: host: localhost port: 6379 password: 123 datasource: url: jdbc:mysql://127.0.0.1:3306/hq_oauth?characterEncoding=UTF-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver #tomcat: #max-idle: 5 #max-wait: 10000 #min-idle: 2 #initial-size: 3 #validation-query: select 1 eureka: client: service-url: defaultZone: http://localhost:8888/eureka/ logging: level: com.hq.*: debug feign: hystrix: enabled: true
途中遇到的問題
spring security 5 版本帶來的問題,若是你想用內存建立用戶或者客戶端信息,能夠能遇到密碼不匹配的問題,請參考:升級到spring security5遇到的坑-密碼存儲格式spring
結果
使用postman請求獲取token:
訪問地址:http://localhost:8000/oauth/token?username=huang&password=123456&grant_type=password&scope=xx;sql
獲取令牌後的訪問用戶信息:數據庫