知识点
在进行实战前,先说明一些本文关于微服务的知识点
-
网关微服务
即Spring Cloud Gateway,用户对微服务的请求几乎都要经由网关,让网关来做转发,由网关提供统一的入口,能起到内部和外部隔离,保证了后台多个服务的安全性。
-
认证中心微服务
用户在访问受权限保护的接口时,要向认证中心认证本人信息并申请访问授权,微服务系统中使用统一的认证中心可以让用户只需一次登录,持有令牌即可访问系统内的所有微服务,避免了不同服务还要重复登录的繁琐。
认证中心只负责两件事:认证和鉴权,认证是指对用户登录提交的如用户名密码进行校验,验证是否为系统内的合法用户,认证通过则返回表示通过的令牌,如JWT;鉴权指用户持有令牌访问微服务接口资源时,需要判断用户所持有的令牌是否拥有访问该资源的权限。
-
资源服务
就是微服务系统下的各种微服务,如商城系统中的订单服务、商品服务。
资源服务提供接口供外部访问,需要依赖认证中心做访问鉴权,当然某些不需要认证的接口例外。
微服务认证授权的一些实现方案
微服务的认证授权可以有以下几种实现方案:
-
不需要网关服务,只搭建认证中心和资源服务
通常不会这么做,网关在微服务系统中还是十分重要的,可以统一入口、降低客户端和服务的耦合、鉴权校验等,所以该方案一般不可取。
-
分别搭建网关服务、认证中心和资源服务
由网关负责转发请求,认证中心和资源服务各司其职,该方案比较主流,但缺点是服务较多,搭建和运维比较麻烦,所以对于小一点的项目来说,或许没有这个必要,可以考虑整合。
-
网关服务与认证中心整合,资源服务独立搭建
是一种比较实用的方案,因为很少会有庞大的系统要搭建,实际上大家都是小一点的项目,小一点的服务。
为什么要用微服务
单体应用和微服务各有各的好处,我们需要针对待解决的问题来考虑,说下优缺点对比:
单体应用的优缺点
优点:
- 调试方便:所有功能服务都在一起,也方便测试
- 容易部署:更新部署比较简单,打一个包更新即可
缺点:
- 不够灵活:修改配置需要重新部署
- 体积庞大:单体应用包含了所有代码,构建、部署时间长,时间久了也容易使项目变得难以维护
- 依赖限制:项目使用的技术栈过时后,升级难度大
- 服务器成本高:单体应用变得庞大之后,对服务器的配置也随之变高,这时候一般只能通过升级服务器来解决
微服务的优缺点
优点:
- 启动快:服务分离开了,单个服务启动就快了
- 服务职责分离:多个服务负责各自的工作,降低了复杂性,方便开发者进行拆分和管理
- 单台服务器成本低:可以通过多个普通服务器来完成各自的服务功能
- 服务更加可靠:搭建集群,配置主从,使整个系统比单体应用更加稳定可靠
缺点:
- 运维难度高:微服务搭建的分布式系统,系统容错、网络延迟、分布式事务等问题需要考虑
- 管理难度高:微服务过多,治理的难度也单体应用要高
微服务是现在后端技术发展的未来趋势或者说是主流,并且我个人认为,开发微服务还有一个好处,就是不用太担心接手的项目有多难搞,因为使用微服务的项目一般不会是多大的屎山,顶多是个小屎山,大不了重构就是。
Spring Cloud OAuth2实战
太罗嗦了,现在开始实战。
实现内容是:创建一个Maven项目,搭建两个服务,一个是认证中心服务,一个是资源服务。
认证中心提供以下接口:
- 登录认证,返回JWT令牌
- 验证JWT令牌有效性
- 刷新JWT令牌
资源服务提供以下接口:
- /a/hello:返回a hello字符串,该接口需要认证才能访问
- /b/hello:返回b hello字符串,该接口不需要认证就能访问
步骤一:创建Maven项目
只给pom.xml参考:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cc</groupId>
<artifactId>oauth2Demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>auth-server</module>
<module>client-server</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
<relativePath/>
</parent>
<properties>
<!--微服务版本-->
<spring-cloud.version>Hoxton.SR5</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.0.RELEASE</spring-cloud-alibaba.version>
<spring-cloud-oauth2.version>2.1.4.RELEASE</spring-cloud-oauth2.version>
</properties>
<!--dependencyManagement中声明jar包的版本号等信息,子项目再次引用该依赖时就不用显示的列出版本号-->
<dependencyManagement>
<dependencies>
<!--Spring Cloud 相关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--Spring Cloud Alibaba 相关依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>${spring-cloud-oauth2.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
我们不依赖JDBC是因为我们为了方便演示,全部数据都写在内存里面
步骤二:创建认证中心服务
pom.xml
<dependencies>
<!--认证中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--springboot启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
HTTP请求访问配置
package com.cc.config.oauth2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密策略
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 鉴权管理器,鉴权交给网关或者资源服务,所以这里不做额外配置,但是需要初始化并注入
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
/**
* http请求访问配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
/**
* 三种写法:
* anyRequest().permitAll() // 放行所有请求
* anyRequest().authenticated() // 所有请求都要认证
* antMatchers("/**").permitAll() //定制
*/
.anyRequest().permitAll()
.and()
// 设置跨域,这里不设置的话,跨域配置不会生效
.cors()
.and()
/**
* 关闭跨站请求保护以及不使用session
* 关闭CSRF防护:使用JWT可以防止CSRF,并且客户端可能不是浏览器,CSRF保护会增加过滤器等其他组件影响性能,所以应该禁用
* 关闭session:使用JWT就可以不用session了,并且客户端可能不是浏览器
*/
.csrf()
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 写入一个测试用户到内存中用于登录,密码是123456
UserDetails userDetails = new User("admin", "$2a$10$CzJxwgp4ZAYhxs/9h6MKjeobgYivUDtthxHT3OXB2oCp02UkKgne2", new ArrayList<>());
auth.inMemoryAuthentication()
.withUser(userDetails).passwordEncoder(passwordEncoder());
}
}
OAuth2服务配置
package com.cc.config.oauth2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import java.util.ArrayList;
import java.util.List;
// 开启认证服务器
@EnableAuthorizationServer
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
// 需要在SecurityConfig中注入Bean
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenEnhancer tokenEnhancer;
@Autowired
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private MyUserDetailsService myUserDetailsService;
/**
* api接口配置:
* 认证服务对外提供api接口,这里对api的访问权限进行配置
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
security
// 支持client_id及client_secret做登录认证
.allowFormAuthenticationForClients()
// 访问/oauth/token_key的处理,可以设置为:permitAll()或者isAuthenticated()
.tokenKeyAccess("permitAll()")
// 访问/oauth/check_token的处理,可以设置为:permitAll()或者isAuthenticated()
.checkTokenAccess("permitAll()");
}
/**
* 客户端配置:
* 这里对多种用户类型进行配置,比如有系统管理员登录、PC平台用户登录、小程序用户登录等
* 配置后的客户端才能被认证服务识别
* 配置可以存储在内存也可以在JDBC中持久化
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
clients.inMemory()
// 客户端id
.withClient("sys_user")
// 客户端密钥,当配置了密码加密策略后,这里的密码需要写加密后的
.secret("$2a$10$CzJxwgp4ZAYhxs/9h6MKjeobgYivUDtthxHT3OXB2oCp02UkKgne2")
// 认证类型,支持OAuth2的四种授权模式,可以配置多个,具体用哪个取决于调用时传的参数
.authorizedGrantTypes("password", "refresh_token")
// 访问令牌的过期时间
.accessTokenValiditySeconds(3600 * 24)
// 刷新令牌的过期时间
.refreshTokenValiditySeconds(3600 * 24 * 7)
// 客户端的权限范围,all即可
.scopes("all")
;
}
/**
* 配置授权、令牌的访问端点
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(tokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancerList);
endpoints
// 允许的请求方式
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
// 指定认证管理器
.authenticationManager(authenticationManager)
.tokenEnhancer(enhancerChain)
.userDetailsService(myUserDetailsService)
;
}
}
实现UserDetailsService接口
编写用户查询逻辑:
package com.cc.config.oauth2;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
@Component
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里模拟根据username从数据库查询用户
// ...
// 然后返回用户,如果查不到记得报错
return new User("admin", "$2a$10$CzJxwgp4ZAYhxs/9h6MKjeobgYivUDtthxHT3OXB2oCp02UkKgne2", new ArrayList<>());
}
}
可选:给JWT添加自定义信息
package com.cc.config.oauth2;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
/**
* JWT加强,添加一些自定义的信息
*
* @author cc
* @date 2023-01-18 15:42
*/
public class MyTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> info = new HashMap<>();
// 通过clientId区分不一样的用户端
String clientId = oAuth2Authentication.getOAuth2Request().getClientId();
switch (clientId) {
case "sys_user": {
info.put("cc", "cc");
}
break;
default:
throw new RuntimeException("clientId不能为空");
}
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
Token令牌设置
package com.cc.config.oauth2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
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.JwtTokenStore;
@Configuration
public class TokenStoreConfig {
/**
* InMemoryTokenStore:默认采用,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
* JdbcTokenStore:这是⼀个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意"spring-jdbc"这个依赖加入到你的 classpath当中。
* JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),缺点就是这个令牌占用的空间会比较大,如果你加⼊了比较多用户凭证信息,JwtTokenStore 不会保存任何数据。
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* JWT 加密密钥设置
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("dev");
return converter;
}
/**
* JWT加强,增加一些自定义拓展
*/
@Bean
public TokenEnhancer tokenEnhancer() {
return new MyTokenEnhancer();
}
}
测试接口
打开postman,测试以下接口:
获取token
验证token
刷新token
注意这里传的是refresh_token。
测试没有问题,那我们就可以继续资源服务的开发了。
步骤二:创建资源服务
pom.xml
<dependencies>
<!--认证中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--springboot启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
资源服务配置类
package com.cc.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
@Configuration
@EnableResourceServer
@EnableWebSecurity
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置当前服务的资源id
resources.resourceId("client_demo");
// 定义token服务对象
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 校验端点url设置,即认证中心接口地址
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8888/oauth/check_token");
// 携带客户端id和客户端密码
remoteTokenServices.setClientId("sys_user");
remoteTokenServices.setClientSecret("123456");
resources.tokenServices(remoteTokenServices);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/a/**").authenticated()
.antMatchers("/b/**").permitAll()
.anyRequest().permitAll()
;
}
}
编写接口
package com.cc.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/a/hello")
public String a() {
return "a hello";
}
@GetMapping("/b/hello")
public String b() {
return "b hello";
}
}
测试接口
首先,不带token的去访问/a/hello接口:
很好,报错了,现在我们携带token再来访问一次:
最后测试/b/hello接口,不带token的,因为我们配置了这个接口不需要认证:
都符合我们的逾期。
未完待续
今天的时间不多了,后面再说结合网关和整合网关的示例吧。