導讀:為了保證我們微服務的安全性,本章主要內容是使用Oauth2.0給我們微服務加上安全校驗。
概念
為了保證服務的安全性,往往都會在接口調用時做權限校驗。在分布式架構中我們會把復雜的業務拆成多個微服務,這樣不得不在所有服務中都實現這樣的權限校驗邏輯,這樣就會有很多代碼和功能冗余。所以在微服務架構中一般會獨立出一個單獨的認證授權服務,供其他所有服務調用。
在SpringCloud體系中,我們只對網關層開放外網訪問權限,其他后端微服務做網絡隔離,所有外部請求必須要通過網關才能訪問到后端服務。在網關層對請求進行轉發時先校驗用戶權限,判斷用戶是否有權限訪問。
我們一般使用Oauth2.0來實現對所有后端服務的統一認證授權。這期內容不講Oauth2.0協議,只講實現過程。如果大家對Oauth2.0不是很了解,可以翻看我之前的博客。
Oauth2認證服務
建立認證服務Auth-Service
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--database-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.jianzh5.cloud</groupId>
<artifactId>cloud-common</artifactId>
</dependency>
</dependencies>
引入mysql主要是我們需要將oauth2.0的客戶端信息以及token認證存入數據庫。
建立相關數據表
主要是導入oauth2相關數據表以及用戶表,在實際開發過程中用戶權限這一套應該是基於RBAC進行設計,這里為了方便演示我就直接只做一個用戶表。
(oauth2.0相關表結構大家可以在網上找,如果找不到可以聯系我)
給用戶表添加數據:
INSERT INTO `user` VALUES ('1', '$2a$10$gExKdT3nkoFKfW1cFlqQUuFji3azHG.W4Pe3/WxHKANg3TpkSJRfW', 'zhangjian', 'ADMIN');
注意:在spring-security 5.x版本必須要注入密碼實現器,我們使用了 BCryptPasswordEncoder
加密器,所以需要這里也需要對密碼進行加密
添加client信息,使用 oauth_client_details
表:
INSERT INTO `oauth_client_details` VALUES ('app', 'app', '$2a$10$fG7ou8CNxDESVFLIM7LrneDmIpwbrxGM2W6.coGPddfQPyZxiqXE6', 'web', 'implicit,client_credentials,authorization_code,refresh_token,password', 'http://www.baidu.com', 'ROLE_USER', null, null, null, null);
同理也需要對client_secret字段進行加密
application.yml配置文件
spring:
main:
allow-bean-definition-overriding: true
application:
name: auth-service
cloud:
nacos:
discovery:
server-addr: xx.xx.xx.xx:8848/
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://xx.xx.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: xxxxxx
driver-class-name: com.mysql.jdbc.Driver
server:
port: 5000
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
自定義認證服務器
只需要繼承 AuthorizationServerConfigurerAdapter
並在開始處加上@EnableAuthorizationServer
注解即可
/** * <p> * <code>AuthorizationServerConfig</code> * </p> * Description: * 授權/認證服務器配置 * @author javadaily * @date 2020/2/26 16:26 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailService;
// 認證管理器
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
/** * access_token存儲器 * 這里存儲在數據庫,大家可以結合自己的業務場景考慮將access_token存入數據庫還是redis */
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/** * 從數據庫讀取clientDetails相關配置 * 有InMemoryClientDetailsService 和 JdbcClientDetailsService 兩種方式選擇 */
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/** * 注入密碼加密實現器 */
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/** * 認證服務器Endpoints配置 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//如果需要使用refresh_token模式則需要注入userDetailService
endpoints.userDetailsService(userDetailService);
endpoints.authenticationManager(this.authenticationManager);
endpoints.tokenStore(tokenStore());
}
/** * 認證服務器相關接口權限管理 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients() //如果使用表單認證則需要加上
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
/** * client存儲方式,此處使用jdbc存儲 */
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
}
自定義web安全配置類
/** * <p> * <code>WebSecurityConfig</code> * </p> * Description: * 自定義web安全配置類 * @author javadaily * @date 2020/2/26 16:35 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public UserDetailsService userDetailsService(){
return new UserDetailServiceImpl();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/** * 認證管理 * @return 認證管理對象 * @throws Exception 認證異常信息 */
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
/** * http安全配置 * @param http http安全對象 * @throws Exception http安全異常信息 */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().httpBasic()
.and().cors()
.and().csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/error",
"/static/**",
"/v2/api-docs/**",
"/swagger-resources/**",
"/webjars/**",
"/favicon.ico"
);
}
}
自定義用戶實現
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//獲取本地用戶
User user = userMapper.selectByUserName(userName);
if(user != null){
//返回oauth2的用戶
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList(user.getRole())) ;
}else{
throw new UsernameNotFoundException("用戶["+userName+"]不存在");
}
}
}
實現 UserDetailsService
接口並實現 loadUserByUsername
方法,這一部分大家根據自己的技術框架實現,Dao層我就不貼出來了。
對外提供獲取當前用戶接口
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
public UserMapper userMapper;
@GetMapping("getByName")
public User getByName(){
return userMapper.selectByUserName("zhangjian");
}
/** * 獲取授權的用戶信息 * @param principal 當前用戶 * @return 授權信息 */
@GetMapping("current/get")
public Principal user(Principal principal){
return principal;
}
}
資源服務器
Oauth2.0的認證服務器也資源服務器,我們在啟動類上加入 @EnableResourceServer
注解即可
@SpringBootApplication
//對外開啟暴露獲取token的API接口
@EnableResourceServer
@EnableDiscoveryClient
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
后端微服務改造
后端所有微服務都是資源服務器,所以我們需要對其進行改造,下面以account-service為例說明改造過程
- 添加oauth2.0依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
- 配置資源服務器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers(
"/v2/api-docs/**",
"/swagger-resources/**",
"/swagger-ui.html",
"/webjars/**"
).permitAll()
.anyRequest().authenticated()
.and()
//統一自定義異常
.exceptionHandling()
.and()
.csrf().disable();
}
}
- 修改配置文件,配置身份獲取地址
security:
oauth2:
resource:
user-info-uri: http://localhost:5000/user/current/get
id: account-service
測試
-
我們直接訪問account-service接口,會提示需要需要認證授權
-
通過密碼模式從認證服務器獲取access_token
-
在請求頭上帶上access_token重新訪問account-service接口,接口正常響應
-
通過debug模式發現每次訪問后端服務時都會去認證資源器獲取當前用戶
-
輸入錯誤的access_token進行訪問,提示access_token失效
總結
通過以上幾步我們將后端服務加上了認證服務,必須要先進行認證才能正常訪問后端服務。
整個實現過程還是比較復雜的,建議大家都實踐一下,理解其中相關配置的作用,也方便更深入理解Oauth2協議。
SpringCloud Alibaba 系列文章
- SpringCloud Alibaba微服務實戰系列