spring-cloud-gateway-oauth2
前言
我们理想的微服务权限解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
架构
通过认证服务(oauth2-auth
)进行统一认证,然后通过网关(oauth2-gateway
)来统一校验认证和鉴权。采用Nacos作为注册中心,Gateway作为网关,使用nimbus-jose-jwtJWT库操作JWT令牌。
- oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合Spring Security Oauth2
- ouath2-gateway:网关服务,负责请求转发和鉴权功能,整合Spring Security Oauth2
- oauth2-resource:受保护的API服务,用户鉴权通过后可以访问该服务,不整合Spring Security Oauth2
具体实现
一、认证服务oauth2-auth
1、首先来搭建认证服务,它将作为Oauth2的认证服务使用,并且网关服务的鉴权功能也需要依赖它,在pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.16</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
2、在application.yml中添加相关配置,主要是Nacos和Redis相关配置
server:
port: 9401
spring:
profiles:
active: dev
application:
name: oauth2-auth
cloud:
nacos:
discovery:
server-addr: localhost:8848
jackson:
date-format: yyyy-MM-dd HH:mm:ss
redis:
database: 0
port: 6379
host: localhost
password:
management:
endpoints:
web:
exposure:
include: "*"
3、使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
4、创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息
package cn.gathub.auth.service.impl;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import cn.gathub.auth.constant.MessageConstant;
import cn.gathub.auth.domain.entity.User;
import cn.gathub.auth.service.UserService;
import cn.gathub.auth.service.principal.UserPrincipal;
import cn.hutool.core.collection.CollUtil;
/**
* 用户管理业务类
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Service
public class UserServiceImpl implements UserService {
private List<User> userList;
private final PasswordEncoder passwordEncoder;
public UserServiceImpl(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@PostConstruct
public void initData() {
String password = passwordEncoder.encode("123456");
userList = new ArrayList<>();
userList.add(new User(1L, "admin", password, 1, CollUtil.toList("ADMIN")));
userList.add(new User(2L, "user", password, 1, CollUtil.toList("USER")));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<User> findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());
if (CollUtil.isEmpty(findUserList)) {
throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);
}
UserPrincipal userPrincipal = new UserPrincipal(findUserList.get(0));
if (!userPrincipal.isEnabled()) {
throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);
} else if (!userPrincipal.isAccountNonLocked()) {
throw new LockedException(MessageConstant.ACCOUNT_LOCKED);
} else if (!userPrincipal.isAccountNonExpired()) {
throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);
} else if (!userPrincipal.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);
}
return userPrincipal;
}
}
5、创建ClientServiceImpl类实现Spring Security的ClientDetailsService接口,用于加载客户端信息
package cn.gathub.auth.service.impl;
import org.springframework.http.HttpStatus;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import cn.gathub.auth.constant.MessageConstant;
import cn.gathub.auth.domain.entity.Client;
import cn.gathub.auth.service.ClientService;
import cn.gathub.auth.service.principal.ClientPrincipal;
import cn.hutool.core.collection.CollUtil;
/**
* 客户端管理业务类
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/18
*/
@Service
public class ClientServiceImpl implements ClientService {
private List<Client> clientList;
private final PasswordEncoder passwordEncoder;
public ClientServiceImpl(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@PostConstruct
public void initData() {
String clientSecret = passwordEncoder.encode("123456");
clientList = new ArrayList<>();
// 1、密码模式
clientList.add(Client.builder()
.clientId("client-app")
.resourceIds("oauth2-resource")
.secretRequire(false)
.clientSecret(clientSecret)
.scopeRequire(false)
.scope("all")
.authorizedGrantTypes("password,refresh_token")
.authorities("ADMIN,USER")
.accessTokenValidity(3600)
.refreshTokenValidity(86400).build());
// 2、授权码模式
clientList.add(Client.builder()
.clientId("client-app-2")
.resourceIds("oauth2-resource2")
.secretRequire(false)
.clientSecret(clientSecret)
.scopeRequire(false)
.scope("all")
.authorizedGrantTypes("authorization_code,refresh_token")
.webServerRedirectUri("https://www.gathub.cn,https://www.baidu.com")
.authorities("USER")
.accessTokenValidity(3600)
.refreshTokenValidity(86400).build());
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
List<Client> findClientList = clientList.stream().filter(item -> item.getClientId().equals(clientId)).collect(Collectors.toList());
if (CollUtil.isEmpty(findClientList)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, MessageConstant.NOT_FOUND_CLIENT);
}
return ne