该篇文章主要记录,使用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。
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 实现访问资源之前的拦截配置。该拦截器的顺序在资源服务器拦截器之前。
代码如下:
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,表结构如下:
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);
配置如下:
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校验
这边我只是做个简单的判断。
@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遇到的坑-密码存储格式
结果
使用postman请求获取token:
访问地址:http://localhost:8000/oauth/token?username=huang&password=123456&grant_type=password&scope=xx;
获取令牌后的访问用户信息:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。