参考《深入理解Spring Cloud与微服务构建》 感谢作者 方志朋
参考 https://blog.csdn.net/yuanlaijike/category_9283872.html
参考 https://www.jianshu.com/p/19059060036b
本文是下文的进阶篇,进一步以微服务为基础进行集成Spring Security + Oauth2 + JWT+Swagger2 + Druid
微服务自动化部署SpringCloud+Dockerfile+docker-compose+git+Maven
把网关服务和验证服务集成到一个微服务中,验证服务以oauth2+jwt进行实现,同时使用swagger2简单明了的展示验证登入的相关接口,druid方便管理数据库连接池以及性能排查
//注意 Spring Boot 1.2x的zuul与oauth2集成会报spring注入错误和unable to start embedded tomcat错误
//Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.springframework.cloud.security.oauth2.resource.ResourceServerProperties org.springframework.cloud.security.oauth2.proxy.OAuth2ProxyAutoConfiguration.resourceServerProperties; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [org.springframework.cloud.security.oauth2.resource.ResourceServerProperties] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
//参考:https://github.com/spring-cloud/spring-cloud-security/issues/73
//解决办法是升级 Spring Cloud 版本为Brixton.SR5,Spring Boot版本为1.3.5
//新增模块 authservice,网关zuul可参考ui模块配置
//包含 config、eureka、feign、ribbon、hystrix、zuul
//config模块添加文件 authservice.yml
ribbon:
ReadTimeout: 60000
ConnectTimeout: 20000
zuul:
host:
connect-timeout-millis: 20000
socket-timeout-millis: 60000
routes:
authservice:
path: /uiservice/**
# 相当于把http://localhost/uiservice/xxx中的/xxx段映射到原来的http://localhost/ui/xxx
serviceId: ui
sensitiveHeaders:
//添加依赖oauth2,默认包含security、oauth2、jwt、lombok包
org.springframework.cloud
spring-cloud-starter-oauth2
//oauth2是实现Spring Security验证模块,所以先直接进行Spring Security的配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalMethodSecurityConfiguration {
}
@Configuration
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
})
.and()
.authorizeRequests()
.antMatchers("/**").authenticated()
.and()
.httpBasic();
}
@Autowired
UserServiceDetail userServiceDetail;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userServiceDetail)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
//注意上面注入的userServiceDetail需要自己实现接口UserDetailsService,security jpa实体表可以自动创建,但是oauth的表需要自行创建,语句见源码的sql.sql
@Service
public class UserServiceDetail implements UserDetailsService {
@Autowired
private UserDao userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username);
}
}
//同时也是资源服务器,配置如下
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter{
Logger log = LoggerFactory.getLogger(ResourceServerConfiguration.class);
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.regexMatchers(".*swagger.*",".*v2.*",".*webjars.*","/user/login.*","/user/registry.*","/user/test.*",".*druid.*").permitAll()
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
log.info("Configuring ResourceServerSecurityConfigurer ");
resources.resourceId("authservice").tokenStore(tokenStore);
}
@Autowired
TokenStore tokenStore;
}
//oauth2配置,定义内部oauth server的basic验证用户名密码,并加入jwt私钥解密方式
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("authservice")
.secret("123456")
.scopes("service")
.autoApprove(true)
.authorizedGrantTypes("implicit","refresh_token", "password", "authorization_code")
.accessTokenValiditySeconds(24*3600);//24小时过期
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).tokenEnhancer(jwtTokenEnhancer()).authenticationManager(authenticationManager);
}
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("fzp-jwt.jks"), "fzp123".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("fzp-jwt"));
return converter;
}
}
//jwt配置,公钥加密方式
@Configuration
public class JwtConfiguration {
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
System.out.println("Created JwtTokenStore");
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey ;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
}
//加入依赖
io.springfox
springfox-swagger2
2.7.0
io.springfox
springfox-swagger-ui
2.7.0
//配置swagger2
@Configuration
@EnableSwagger2
public class SwaggerConfig {
/**
* 全局参数
*
* @return
*/
private List parameter() {
List params = new ArrayList<>();
params.add(new ParameterBuilder().name("Authorization")
.description("Authorization Bearer token")
.modelRef(new ModelRef("string"))
.parameterType("header")
.required(false).build());
return params;
}
@Bean
public Docket sysApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.xiaofeng.authservice.controller"))
.paths(PathSelectors.any())
.build().globalOperationParameters(parameter());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(" authservice api ")
.description("authservice 微服务")
.termsOfServiceUrl("")
.contact("xiaofeng")
.version("1.0")
.build();
}
}
//添加controller
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@ApiOperation(value = "注册", notes = "username和password为必选项")
@RequestMapping(value = "/registry",method = RequestMethod.POST)
public User createUser(@RequestBody User user){
//参数判读省略,判读该用户在数据库是否已经存在省略
String entryPassword= BPwdEncoderUtils.BCryptPassword(user.getPassword());
user.setPassword(entryPassword);
return userService.createUser(user);
}
@ApiOperation(value = "登录", notes = "username和password为必选项")
@RequestMapping(value = "/login",method = RequestMethod.POST)
public RespDTO login(@RequestParam String username , @RequestParam String password){
//参数判读省略
return userService.login(username,password);
}
@ApiOperation(value = "根据用户名获取用户", notes = "根据用户名获取用户")
@RequestMapping(value = "/{username}",method = RequestMethod.POST)
@PreAuthorize("hasRole('USER')")
// @PreAuthorize("hasAnyAuthority('ROLE_USER')")
public RespDTO getUserInfo(@PathVariable("username") String username){
//参数判读省略
User user= userService.getUserInfo(username);
return RespDTO.onSuc(user);
}
}
//数据库连接池druid
//config模块的文件 authservice.yml添加
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud?useUnicode=true&characterEncoding=utf-8
username: root
password: root
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,log4j
connectionProperties:
druid.stat.mergeSql: true
druid.stat.slowSqlMillis: 5000
useGlobalDataSourceStat: true
jpa:
hibernate:
ddl-auto: update
show-sql: true
//Druid配置
@Configuration
public class DruidConfiguration {
@Bean
public ServletRegistrationBean DruidStatViewServle2() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
//白名单:
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
//IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page.
//servletRegistrationBean.addInitParameter("deny", "192.168.1.73");
//登录查看信息的账号密码.
servletRegistrationBean.addInitParameter("loginUsername", "admin2");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
//是否能够重置数据.
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
/**
* 注册一个:filterRegistrationBean
*
* @return
*/
@Bean
public FilterRegistrationBean druidStatFilter2() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
//添加过滤规则.
filterRegistrationBean.addUrlPatterns("/*");
//添加不需要忽略的格式信息.
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
//模拟登入
curl -X POST -d username=admin -d password=admin http://localhost:9999/user/login
//模拟从oauth server 获取token
curl -X POST -u authservice:123456 http://localhost:9999/oauth/token -d grant_type=password -d username=admin -d password=admin
//模拟从oauth server 获取token 另一种写法
curl -X POST -H "Authorization: Basic YXV0aHNlcnZpY2U6MTIzNDU2" -d grant_type=password -d username=admin -d password=admin http://localhost:9999/oauth/token
//模拟用登入得到token进行访问有权限的controll接口
curl -X POST -H "Authorization:BearereyJhbGciOiJSUzI1NiJ9.eyJleHAiOjE1Nzg1Mzc4NDIsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiVVNFUiJdLCJqdGkiOiI4Y2JkYjRjMy0wOTMxLTQyMWItYWYwOS1lMDViYzYyODFhY2YiLCJjbGllbnRfaWQiOiJhdXRoc2VydmljZSIsInNjb3BlIjpbInNlcnZpY2UiXX0.JNdRVRcOau0uTmUdSVdImeQPcIf6PcBzIL3bdfm0856ou9EjEiGDbqike1nw2DueR3Kq5AnbtsYuiPA_sEuwamFLy3H2Eezo7y-5DX26fId7PufHWn1aSshsW5zQNGORr47xZ8_oXq2J5yfwzCrDNDzqbgkcOAB7jWTD9DcOPUig2FCvA0AglZxt442W34N_Sds6l8C6Hy9Dl2hlzAoe0VCy_yCv2APnwNhX4KWnFJTZsEK9LeYgwvlM0nPz6JOYwOlLSk4P8geC0zuspoJ0Ve9mXU4qHzX040amrSjnJooLL1jmsxDVffop6rprkQmuKSkEDfipVfRLx5TUB9xv4g","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInNlcnZpY2UiXSwiYXRpIjoiOGNiZGI0YzMtMDkzMS00MjFiLWFmMDktZTA1YmM2MjgxYWNmIiwiZXhwIjoxNTgxMDQzNDQyLCJhdXRob3JpdGllcyI6WyJVU0VSIl0sImp0aSI6IjFjNzc3ZjE4LWU0YjEtNGYzNi05ZTE5LTIwYzZiNDRmZGMyOCIsImNsaWVudF9pZCI6ImF1dGhzZXJ2aWNlIn0.YmcMlCzEmLtN_W6XrlP3O5EkH_jbH6Gpa8rxiuVRpW6k9R3k77ZZauE07f1v_dyUL3DGRzuMMGDGfKHOUapJ9gus2UX9-QDe9x9V46hEVkfcHplYwwdC43o8Z6URM4rlA5vJkKbQa6EI1KJVZfLNkfTSXjE2TD3M2MuwJu4xgkNg6Eg25vDxyiFPIsyOBIl66ROJJogS90M7tMrOiCTK40jWTPwrDfOdy7EzJvi0mlCwZmGK9qP2pwB8yi5zcgHT2P0XrPFT_VPDQJS5X7DLU_k-k_mfoHyobtVHoDF7VkUdLngtoqy_ynF-hcJqvHH8PvnwjaVzi448dGpXXBSaxA" http://localhost:9999/user/admin