架构
通过认证服务(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
代码语言:txt复制1、首先来搭建认证服务,它将作为Oauth2的认证服务使用,并且网关服务的鉴权功能也需要依赖它,在pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖
<dependencies>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.boot</groupId>代码语言:txt复制 <artifactId>spring-boot-starter-web</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.boot</groupId>代码语言:txt复制 <artifactId>spring-boot-starter-security</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.cloud</groupId>代码语言:txt复制 <artifactId>spring-cloud-starter-oauth2</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>com.nimbusds</groupId>代码语言:txt复制 <artifactId>nimbus-jose-jwt</artifactId>代码语言:txt复制 <version>8.16</version>代码语言:txt复制 </dependency>代码语言:txt复制 <!-- redis -->代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.boot</groupId>代码语言:txt复制 <artifactId>spring-boot-starter-data-redis</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制</dependencies>代码语言:txt复制2、在application.yml中添加相关配置,主要是Nacos和Redis相关配置
server:代码语言:txt复制 port: 9401代码语言:txt复制spring:代码语言:txt复制 profiles:代码语言:txt复制 active: dev代码语言:txt复制 application:代码语言:txt复制 name: oauth2-auth代码语言:txt复制 cloud:代码语言:txt复制 nacos:代码语言:txt复制 discovery:代码语言:txt复制 server-addr: localhost:8848代码语言:txt复制 jackson:代码语言:txt复制 date-format: yyyy-MM-dd HH:mm:ss代码语言:txt复制 redis:代码语言:txt复制 database: 0代码语言:txt复制 port: 6379代码语言:txt复制 host: localhost代码语言:txt复制 password:代码语言:txt复制management:代码语言:txt复制 endpoints:代码语言:txt复制 web:代码语言:txt复制 exposure:代码语言:txt复制 include: "*"代码语言:txt复制3、使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks代码语言:txt复制4、创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息
package cn.gathub.auth.service.impl;代码语言:txt复制import org.springframework.security.authentication.AccountExpiredException;代码语言:txt复制import org.springframework.security.authentication.CredentialsExpiredException;代码语言:txt复制import org.springframework.security.authentication.DisabledException;代码语言:txt复制import org.springframework.security.authentication.LockedException;代码语言:txt复制import org.springframework.security.core.userdetails.UserDetails;代码语言:txt复制import org.springframework.security.core.userdetails.UsernameNotFoundException;代码语言:txt复制import org.springframework.security.crypto.password.PasswordEncoder;代码语言:txt复制import org.springframework.stereotype.Service;代码语言:txt复制import java.util.ArrayList;代码语言:txt复制import java.util.List;代码语言:txt复制import java.util.stream.Collectors;代码语言:txt复制import javax.annotation.PostConstruct;代码语言:txt复制import cn.gathub.auth.constant.MessageConstant;代码语言:txt复制import cn.gathub.auth.domain.entity.User;代码语言:txt复制import cn.gathub.auth.service.UserService;代码语言:txt复制import cn.gathub.auth.service.principal.UserPrincipal;代码语言:txt复制import cn.hutool.core.collection.CollUtil;代码语言:txt复制/**代码语言:txt复制 * 用户管理业务类
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Service
public class UserServiceImpl implements UserService {代码语言:txt复制 private List<User> userList;代码语言:txt复制 private final PasswordEncoder passwordEncoder;代码语言:txt复制 public UserServiceImpl(PasswordEncoder passwordEncoder) {代码语言:txt复制 this.passwordEncoder = passwordEncoder;代码语言:txt复制 }代码语言:txt复制 @PostConstruct代码语言:txt复制 public void initData() {代码语言:txt复制 String password = passwordEncoder.encode("123456");代码语言:txt复制 userList = new ArrayList<>();代码语言:txt复制 userList.add(new User(1L, "admin", password, 1, CollUtil.toList("ADMIN")));代码语言:txt复制 userList.add(new User(2L, "user", password, 1, CollUtil.toList("USER")));代码语言:txt复制 }代码语言:txt复制 @Override代码语言:txt复制 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {代码语言:txt复制 List<User> findUserList = userList.stream().filter(item -> item.getUsername().equals(username)).collect(Collectors.toList());代码语言:txt复制 if (CollUtil.isEmpty(findUserList)) {代码语言:txt复制 throw new UsernameNotFoundException(MessageConstant.USERNAME_PASSWORD_ERROR);代码语言:txt复制 }代码语言:txt复制 UserPrincipal userPrincipal = new UserPrincipal(findUserList.get(0));代码语言:txt复制 if (!userPrincipal.isEnabled()) {代码语言:txt复制 throw new DisabledException(MessageConstant.ACCOUNT_DISABLED);代码语言:txt复制 } else if (!userPrincipal.isAccountNonLocked()) {代码语言:txt复制 throw new LockedException(MessageConstant.ACCOUNT_LOCKED);代码语言:txt复制 } else if (!userPrincipal.isAccountNonExpired()) {代码语言:txt复制 throw new AccountExpiredException(MessageConstant.ACCOUNT_EXPIRED);代码语言:txt复制 } else if (!userPrincipal.isCredentialsNonExpired()) {代码语言:txt复制 throw new CredentialsExpiredException(MessageConstant.CREDENTIALS_EXPIRED);代码语言:txt复制 }代码语言:txt复制 return userPrincipal;代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制5、创建ClientServiceImpl类实现Spring Security的ClientDetailsService接口,用于加载客户端信息
package cn.gathub.auth.service.impl;代码语言:txt复制import org.springframework.http.HttpStatus;代码语言:txt复制import org.springframework.security.crypto.password.PasswordEncoder;代码语言:txt复制import org.springframework.security.oauth2.provider.ClientDetails;代码语言:txt复制import org.springframework.security.oauth2.provider.ClientRegistrationException;代码语言:txt复制import org.springframework.stereotype.Service;代码语言:txt复制import org.springframework.web.server.ResponseStatusException;代码语言:txt复制import java.util.ArrayList;代码语言:txt复制import java.util.List;代码语言:txt复制import java.util.stream.Collectors;代码语言:txt复制import javax.annotation.PostConstruct;代码语言:txt复制import cn.gathub.auth.constant.MessageConstant;代码语言:txt复制import cn.gathub.auth.domain.entity.Client;代码语言:txt复制import cn.gathub.auth.service.ClientService;代码语言:txt复制import cn.gathub.auth.service.principal.ClientPrincipal;代码语言:txt复制import cn.hutool.core.collection.CollUtil;代码语言:txt复制/**代码语言:txt复制 * 客户端管理业务类
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/18
*/
@Service
public class ClientServiceImpl implements ClientService {代码语言:txt复制 private List<Client> clientList;代码语言:txt复制 private final PasswordEncoder passwordEncoder;代码语言:txt复制 public ClientServiceImpl(PasswordEncoder passwordEncoder) {代码语言:txt复制 this.passwordEncoder = passwordEncoder;代码语言:txt复制 }代码语言:txt复制 @PostConstruct代码语言:txt复制 public void initData() {代码语言:txt复制 String clientSecret = passwordEncoder.encode("123456");代码语言:txt复制 clientList = new ArrayList<>();代码语言:txt复制 // 1、密码模式代码语言:txt复制 clientList.add(Client.builder()代码语言:txt复制 .clientId("client-app")代码语言:txt复制 .resourceIds("oauth2-resource")代码语言:txt复制 .secretRequire(false)代码语言:txt复制 .clientSecret(clientSecret)代码语言:txt复制 .scopeRequire(false)代码语言:txt复制 .scope("all")代码语言:txt复制 .authorizedGrantTypes("password,refresh_token")代码语言:txt复制 .authorities("ADMIN,USER")代码语言:txt复制 .accessTokenValidity(3600)代码语言:txt复制 .refreshTokenValidity(86400).build());代码语言:txt复制 // 2、授权码模式代码语言:txt复制 clientList.add(Client.builder()代码语言:txt复制 .clientId("client-app-2")代码语言:txt复制 .resourceIds("oauth2-resource2")代码语言:txt复制 .secretRequire(false)代码语言:txt复制 .clientSecret(clientSecret)代码语言:txt复制 .scopeRequire(false)代码语言:txt复制 .scope("all")代码语言:txt复制 .authorizedGrantTypes("authorization_code,refresh_token")代码语言:txt复制 .webServerRedirectUri("https://www.gathub.cn,https://www.baidu.com")代码语言:txt复制 .authorities("USER")代码语言:txt复制 .accessTokenValidity(3600)代码语言:txt复制 .refreshTokenValidity(86400).build());代码语言:txt复制 }代码语言:txt复制 @Override代码语言:txt复制 public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {代码语言:txt复制 List<Client> findClientList = clientList.stream().filter(item -> item.getClientId().equals(clientId)).collect(Collectors.toList());代码语言:txt复制 if (CollUtil.isEmpty(findClientList)) {代码语言:txt复制 throw new ResponseStatusException(HttpStatus.NOT_FOUND, MessageConstant.NOT_FOUND_CLIENT);代码语言:txt复制 }代码语言:txt复制 return new ClientPrincipal(findClientList.get(0));代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制6、添加认证服务相关配置Oauth2ServerConfig,需要配置加载用户信息的服务UserServiceImpl和加载客户端信息的服务ClientServiceImpl及RSA的钥匙对KeyPair
package cn.gathub.auth.config;代码语言:txt复制import org.springframework.context.annotation.Bean;代码语言:txt复制import org.springframework.context.annotation.Configuration;代码语言:txt复制import org.springframework.core.io.ClassPathResource;代码语言:txt复制import org.springframework.security.authentication.AuthenticationManager;代码语言:txt复制import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;代码语言:txt复制import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;代码语言:txt复制import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;代码语言:txt复制import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;代码语言:txt复制import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;代码语言:txt复制import org.springframework.security.oauth2.provider.token.TokenEnhancer;代码语言:txt复制import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;代码语言:txt复制import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;代码语言:txt复制import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;代码语言:txt复制import java.security.KeyPair;代码语言:txt复制import java.util.ArrayList;代码语言:txt复制import java.util.List;代码语言:txt复制import cn.gathub.auth.component.JwtTokenEnhancer;代码语言:txt复制import cn.gathub.auth.service.ClientService;代码语言:txt复制import cn.gathub.auth.service.UserService;代码语言:txt复制import lombok.AllArgsConstructor;代码语言:txt复制/**代码语言:txt复制 * 认证服务器配置
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {代码语言:txt复制 private final UserService userService;代码语言:txt复制 private final ClientService clientService;代码语言:txt复制 private final AuthenticationManager authenticationManager;代码语言:txt复制 private final JwtTokenEnhancer jwtTokenEnhancer;代码语言:txt复制 @Override代码语言:txt复制 public void configure(ClientDetailsServiceConfigurer clients) throws Exception {代码语言:txt复制// clients.inMemory()代码语言:txt复制// // 1、密码模式代码语言:txt复制// .withClient("client-app")代码语言:txt复制// .secret(passwordEncoder.encode("123456"))代码语言:txt复制// .scopes("read,write")代码语言:txt复制// .authorizedGrantTypes("password", "refresh_token")代码语言:txt复制// .accessTokenValiditySeconds(3600)代码语言:txt复制// .refreshTokenValiditySeconds(86400)代码语言:txt复制// .and()代码语言:txt复制// // 2、授权码授权代码语言:txt复制// .withClient("client-app-2")代码语言:txt复制// .secret(passwordEncoder.encode("123456"))代码语言:txt复制// .scopes("read")代码语言:txt复制// .authorizedGrantTypes("authorization_code", "refresh_token")代码语言:txt复制// .accessTokenValiditySeconds(3600)代码语言:txt复制// .refreshTokenValiditySeconds(86400)代码语言:txt复制// .redirectUris("https://www.gathub.cn", "https://www.baidu.com");代码语言:txt复制 clients.withClientDetails(clientService);代码语言:txt复制 }代码语言:txt复制 @Override代码语言:txt复制 public void configure(AuthorizationServerEndpointsConfigurer endpoints) {代码语言:txt复制 TokenEnhancerChain enhancerChain = new TokenEnhancerChain();代码语言:txt复制 List<TokenEnhancer> delegates = new ArrayList<>();代码语言:txt复制 delegates.add(jwtTokenEnhancer);代码语言:txt复制 delegates.add(accessTokenConverter());代码语言:txt复制 enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器代码语言:txt复制 endpoints.authenticationManager(authenticationManager)代码语言:txt复制 .userDetailsService(userService) //配置加载用户信息的服务代码语言:txt复制 .accessTokenConverter(accessTokenConverter())代码语言:txt复制 .tokenEnhancer(enhancerChain);代码语言:txt复制 }代码语言:txt复制 @Override代码语言:txt复制 public void configure(AuthorizationServerSecurityConfigurer security) {代码语言:txt复制 security.allowFormAuthenticationForClients();代码语言:txt复制 }代码语言:txt复制 @Bean代码语言:txt复制 public JwtAccessTokenConverter accessTokenConverter() {代码语言:txt复制 JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();代码语言:txt复制 jwtAccessTokenConverter.setKeyPair(keyPair());代码语言:txt复制 return jwtAccessTokenConverter;代码语言:txt复制 }代码语言:txt复制 @Bean代码语言:txt复制 public KeyPair keyPair() {代码语言:txt复制 // 从classpath下的证书中获取秘钥对代码语言:txt复制 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "654321".toCharArray());代码语言:txt复制 return keyStoreKeyFactory.getKeyPair("jwt", "654321".toCharArray());代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制7、如果你想往JWT中添加自定义信息的话,比如说登录用户的ID,可以自己实现TokenEnhancer接口
package cn.gathub.auth.component;代码语言:txt复制import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;代码语言:txt复制import org.springframework.security.oauth2.common.OAuth2AccessToken;代码语言:txt复制import org.springframework.security.oauth2.provider.OAuth2Authentication;代码语言:txt复制import org.springframework.security.oauth2.provider.token.TokenEnhancer;代码语言:txt复制import org.springframework.stereotype.Component;代码语言:txt复制import java.util.HashMap;代码语言:txt复制import java.util.Map;代码语言:txt复制import cn.gathub.auth.service.principal.UserPrincipal;代码语言:txt复制/**代码语言:txt复制 * JWT内容增强器
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Map<String, Object> info = new HashMap<>();
// 把用户ID设置到JWT中
info.put("id", userPrincipal.getId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}代码语言:txt复制8、由于我们的网关服务需要RSA的公钥来验证签名是否合法,所以认证服务需要有个接口把公钥暴露出来
package cn.gathub.auth.controller;代码语言:txt复制import com.nimbusds.jose.jwk.JWKSet;代码语言:txt复制import com.nimbusds.jose.jwk.RSAKey;代码语言:txt复制import org.springframework.web.bind.annotation.GetMapping;代码语言:txt复制import org.springframework.web.bind.annotation.RestController;代码语言:txt复制import java.security.KeyPair;代码语言:txt复制import java.security.interfaces.RSAPublicKey;代码语言:txt复制import java.util.Map;代码语言:txt复制/**代码语言:txt复制 * 获取RSA公钥接口
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@RestController
public class KeyPairController {代码语言:txt复制 private final KeyPair keyPair;代码语言:txt复制 public KeyPairController(KeyPair keyPair) {代码语言:txt复制 this.keyPair = keyPair;代码语言:txt复制 }代码语言:txt复制 @GetMapping("/rsa/publicKey")代码语言:txt复制 public Map<String, Object> getKey() {代码语言:txt复制 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();代码语言:txt复制 RSAKey key = new RSAKey.Builder(publicKey).build();代码语言:txt复制 return new JWKSet(key).toJSONObject();代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制9、还需要配置Spring Security,允许获取公钥接口的访问
package cn.gathub.auth.config;代码语言:txt复制import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest;代码语言:txt复制import org.springframework.context.annotation.Bean;代码语言:txt复制import org.springframework.context.annotation.Configuration;代码语言:txt复制import org.springframework.security.authentication.AuthenticationManager;代码语言:txt复制import org.springframework.security.config.annotation.web.builders.HttpSecurity;代码语言:txt复制import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;代码语言:txt复制import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;代码语言:txt复制import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;代码语言:txt复制import org.springframework.security.crypto.password.PasswordEncoder;代码语言:txt复制/**代码语言:txt复制 * SpringSecurity配置
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {代码语言:txt复制 @Override代码语言:txt复制 protected void configure(HttpSecurity http) throws Exception {代码语言:txt复制 http.authorizeRequests()代码语言:txt复制 .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()代码语言:txt复制 .antMatchers("/rsa/publicKey").permitAll()代码语言:txt复制 .anyRequest().authenticated();代码语言:txt复制 }代码语言:txt复制 @Bean代码语言:txt复制 @Override代码语言:txt复制 public AuthenticationManager authenticationManagerBean() throws Exception {代码语言:txt复制 return super.authenticationManagerBean();代码语言:txt复制 }代码语言:txt复制 @Bean代码语言:txt复制 public PasswordEncoder passwordEncoder() {代码语言:txt复制 return new BCryptPasswordEncoder();代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制10、创建一个资源服务ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取
package cn.gathub.auth.service;代码语言:txt复制import org.springframework.data.redis.core.RedisTemplate;代码语言:txt复制import org.springframework.stereotype.Service;代码语言:txt复制import java.util.List;代码语言:txt复制import java.util.Map;代码语言:txt复制import java.util.TreeMap;代码语言:txt复制import javax.annotation.PostConstruct;代码语言:txt复制import cn.gathub.auth.constant.RedisConstant;代码语言:txt复制import cn.hutool.core.collection.CollUtil;代码语言:txt复制/**代码语言:txt复制 * 资源与角色匹配关系管理业务类
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Service
public class ResourceServiceImpl {代码语言:txt复制 private final RedisTemplate<String, Object> redisTemplate;代码语言:txt复制 public ResourceServiceImpl(RedisTemplate<String, Object> redisTemplate) {代码语言:txt复制 this.redisTemplate = redisTemplate;代码语言:txt复制 }代码语言:txt复制 @PostConstruct代码语言:txt复制 public void initData() {代码语言:txt复制 Map<String, List<String>> resourceRolesMap = new TreeMap<>();代码语言:txt复制 resourceRolesMap.put("/resource/hello", CollUtil.toList("ADMIN"));代码语言:txt复制 resourceRolesMap.put("/resource/user/currentUser", CollUtil.toList("ADMIN", "USER"));代码语言:txt复制 redisTemplate.opsForHash().putAll(RedisConstant.RESOURCE_ROLES_MAP, resourceRolesMap);代码语言:txt复制 }代码语言:txt复制}二、网关服务oauth2-gateway
接下来搭建网关服务,它将作为Oauth2的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作
代码语言:txt复制1、在pom.xml中添加相关依赖,主要是Gateway、Oauth2和JWT相关依赖
<dependencies>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.boot</groupId>代码语言:txt复制 <artifactId>spring-boot-starter-webflux</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.cloud</groupId>代码语言:txt复制 <artifactId>spring-cloud-starter-gateway</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.security</groupId>代码语言:txt复制 <artifactId>spring-security-config</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.security</groupId>代码语言:txt复制 <artifactId>spring-security-oauth2-resource-server</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.security</groupId>代码语言:txt复制 <artifactId>spring-security-oauth2-client</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.security</groupId>代码语言:txt复制 <artifactId>spring-security-oauth2-jose</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>com.nimbusds</groupId>代码语言:txt复制 <artifactId>nimbus-jose-jwt</artifactId>代码语言:txt复制 <version>8.16</version>代码语言:txt复制 </dependency>代码语言:txt复制</dependencies>代码语言:txt复制2、在application.yml中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置
server:代码语言:txt复制 port: 9201代码语言:txt复制spring:代码语言:txt复制 profiles:代码语言:txt复制 active: dev代码语言:txt复制 application:代码语言:txt复制 name: oauth2-gateway代码语言:txt复制 cloud:代码语言:txt复制 nacos:代码语言:txt复制 discovery:代码语言:txt复制 server-addr: localhost:8848代码语言:txt复制 gateway:代码语言:txt复制 routes: # 配置路由路径代码语言:txt复制 - id: oauth2-resource-route
uri: lb://oauth2-resource
predicates:
- Path=/resource/**
filters:
- StripPrefix=1
- id: oauth2-auth-route
uri: lb://oauth2-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- id: oauth2-auth-login
uri: lb://oauth2-auth
predicates:
- Path=/login
filters:
- PreserveHostHeader
- id: oauth2-auth-token
uri: lb://oauth2-auth
predicates:
- Path=/oauth/token
filters:
- PreserveHostHeader
- id: oauth2-auth-authorize
uri: lb://oauth2-auth
predicates:
- Path=/oauth/authorize
filters:
- PreserveHostHeader
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能
lower-case-service-id: true # 使用小写服务名,默认是大写
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: 'http://localhost:9401/rsa/publicKey' # 配置RSA的公钥访问地址
redis:
database: 0
port: 6379
host: localhost
password:
secure:
ignore:
urls: # 配置白名单路径
- "/actuator/**"
- "/oauth/token"
- "/oauth/authorize"
- "/login"代码语言:txt复制3、对网关服务进行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启
package cn.gathub.gateway.config;代码语言:txt复制import org.springframework.context.annotation.Bean;代码语言:txt复制import org.springframework.context.annotation.Configuration;代码语言:txt复制import org.springframework.core.convert.converter.Converter;代码语言:txt复制import org.springframework.security.authentication.AbstractAuthenticationToken;代码语言:txt复制import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;代码语言:txt复制import org.springframework.security.config.web.server.SecurityWebFiltersOrder;代码语言:txt复制import org.springframework.security.config.web.server.ServerHttpSecurity;代码语言:txt复制import org.springframework.security.oauth2.jwt.Jwt;代码语言:txt复制import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;代码语言:txt复制import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;代码语言:txt复制import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;代码语言:txt复制import org.springframework.security.web.server.SecurityWebFilterChain;代码语言:txt复制import cn.gathub.gateway.authorization.AuthorizationManager;代码语言:txt复制import cn.gathub.gateway.component.RestAuthenticationEntryPoint;代码语言:txt复制import cn.gathub.gateway.component.RestfulAccessDeniedHandler;代码语言:txt复制import cn.gathub.gateway.constant.AuthConstant;代码语言:txt复制import cn.gathub.gateway.filter.IgnoreUrlsRemoveJwtFilter;代码语言:txt复制import cn.hutool.core.util.ArrayUtil;代码语言:txt复制import lombok.AllArgsConstructor;代码语言:txt复制import reactor.core.publisher.Mono;代码语言:txt复制/**代码语言:txt复制 * 资源服务器配置
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;代码语言:txt复制 @Bean代码语言:txt复制 public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {代码语言:txt复制 http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());代码语言:txt复制 // 1、自定义处理JWT请求头过期或签名错误的结果代码语言:txt复制 http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);代码语言:txt复制 // 2、对白名单路径,直接移除JWT请求头代码语言:txt复制 http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);代码语言:txt复制 http.authorizeExchange()代码语言:txt复制 .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll() // 白名单配置代码语言:txt复制 .anyExchange().access(authorizationManager) // 鉴权管理器配置代码语言:txt复制 .and().exceptionHandling()代码语言:txt复制 .accessDeniedHandler(restfulAccessDeniedHandler) // 处理未授权代码语言:txt复制 .authenticationEntryPoint(restAuthenticationEntryPoint) // 处理未认证代码语言:txt复制 .and().csrf().disable();代码语言:txt复制 return http.build();代码语言:txt复制 }代码语言:txt复制 @Bean代码语言:txt复制 public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {代码语言:txt复制 JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();代码语言:txt复制 jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);代码语言:txt复制 jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);代码语言:txt复制 JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();代码语言:txt复制 jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);代码语言:txt复制 return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制4、在WebFluxSecurity中自定义鉴权操作需要实现ReactiveAuthorizationManager接口
package cn.gathub.gateway.authorization;代码语言:txt复制import org.springframework.data.redis.core.RedisTemplate;代码语言:txt复制import org.springframework.security.authorization.AuthorizationDecision;代码语言:txt复制import org.springframework.security.authorization.ReactiveAuthorizationManager;代码语言:txt复制import org.springframework.security.core.Authentication;代码语言:txt复制import org.springframework.security.core.GrantedAuthority;代码语言:txt复制import org.springframework.security.web.server.authorization.AuthorizationContext;代码语言:txt复制import org.springframework.stereotype.Component;代码语言:txt复制import java.net.URI;代码语言:txt复制import java.util.List;代码语言:txt复制import java.util.stream.Collectors;代码语言:txt复制import cn.gathub.gateway.constant.AuthConstant;代码语言:txt复制import cn.gathub.gateway.constant.RedisConstant;代码语言:txt复制import cn.hutool.core.convert.Convert;代码语言:txt复制import reactor.core.publisher.Mono;代码语言:txt复制/**代码语言:txt复制 * 鉴权管理器,用于判断是否有资源的访问权限
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
private final RedisTemplate<String, Object> redisTemplate;代码语言:txt复制 public AuthorizationManager(RedisTemplate<String, Object> redisTemplate) {代码语言:txt复制 this.redisTemplate = redisTemplate;代码语言:txt复制 }代码语言:txt复制 @Override代码语言:txt复制 public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {代码语言:txt复制 // 1、从Redis中获取当前路径可访问角色列表代码语言:txt复制 URI uri = authorizationContext.getExchange().getRequest().getURI();代码语言:txt复制 Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());代码语言:txt复制 List<String> authorities = Convert.toList(String.class, obj);代码语言:txt复制 authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX i).collect(Collectors.toList());代码语言:txt复制 // 2、认证通过且角色匹配的用户可访问当前路径代码语言:txt复制 return mono代码语言:txt复制 .filter(Authentication::isAuthenticated)代码语言:txt复制 .flatMapIterable(Authentication::getAuthorities)代码语言:txt复制 .map(GrantedAuthority::getAuthority)代码语言:txt复制 .any(authorities::contains)代码语言:txt复制 .map(AuthorizationDecision::new)代码语言:txt复制 .defaultIfEmpty(new AuthorizationDecision(false));代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制5、这里我们还需要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息
package cn.gathub.gateway.filter;代码语言:txt复制import com.nimbusds.jose.JWSObject;代码语言:txt复制import org.slf4j.Logger;代码语言:txt复制import org.slf4j.LoggerFactory;代码语言:txt复制import org.springframework.cloud.gateway.filter.GatewayFilterChain;代码语言:txt复制import org.springframework.cloud.gateway.filter.GlobalFilter;代码语言:txt复制import org.springframework.core.Ordered;代码语言:txt复制import org.springframework.http.server.reactive.ServerHttpRequest;代码语言:txt复制import org.springframework.stereotype.Component;代码语言:txt复制import org.springframework.web.server.ServerWebExchange;代码语言:txt复制import java.text.ParseException;代码语言:txt复制import cn.hutool.core.util.StrUtil;代码语言:txt复制import reactor.core.publisher.Mono;代码语言:txt复制/**代码语言:txt复制 * 将登录用户的JWT转化成用户信息的全局过滤器
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {代码语言:txt复制 private final static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);代码语言:txt复制 @Override代码语言:txt复制 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {代码语言:txt复制 String token = exchange.getRequest().getHeaders().getFirst("Authorization");代码语言:txt复制 if (StrUtil.isEmpty(token)) {代码语言:txt复制 return chain.filter(exchange);代码语言:txt复制 }代码语言:txt复制 try {代码语言:txt复制 // 从token中解析用户信息并设置到Header中去代码语言:txt复制 String realToken = token.replace("Bearer ", "");代码语言:txt复制 JWSObject jwsObject = JWSObject.parse(realToken);代码语言:txt复制 String userStr = jwsObject.getPayload().toString();代码语言:txt复制 LOGGER.info("AuthGlobalFilter.filter() user:{}", userStr);代码语言:txt复制 ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();代码语言:txt复制 exchange = exchange.mutate().request(request).build();代码语言:txt复制 } catch (ParseException e) {代码语言:txt复制 e.printStackTrace();代码语言:txt复制 }代码语言:txt复制 return chain.filter(exchange);代码语言:txt复制 }代码语言:txt复制 @Override代码语言:txt复制 public int getOrder() {代码语言:txt复制 return 0;代码语言:txt复制 }代码语言:txt复制}三、资源服务(API服务)oauth2-resource
最后我们搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它
代码语言:txt复制1、在pom.xml中添加相关依赖,就添加了一个web依赖
<dependencies>代码语言:txt复制 <dependency>代码语言:txt复制 <groupId>org.springframework.boot</groupId>代码语言:txt复制 <artifactId>spring-boot-starter-web</artifactId>代码语言:txt复制 </dependency>代码语言:txt复制</dependencies>代码语言:txt复制2、在application.yml添加相关配置,很常规的配置
server:代码语言:txt复制 port: 9501代码语言:txt复制spring:代码语言:txt复制 profiles:代码语言:txt复制 active: dev代码语言:txt复制 application:代码语言:txt复制 name: oauth2-resource代码语言:txt复制 cloud:代码语言:txt复制 nacos:代码语言:txt复制 discovery:代码语言:txt复制 server-addr: localhost:8848代码语言:txt复制management:代码语言:txt复制 endpoints:代码语言:txt复制 web:代码语言:txt复制 exposure:代码语言:txt复制 include: "*"代码语言:txt复制3、创建一个测试接口,网关验证通过即可访问
package cn.gathub.resource.controller;代码语言:txt复制import org.springframework.web.bind.annotation.GetMapping;代码语言:txt复制import org.springframework.web.bind.annotation.RestController;代码语言:txt复制/**代码语言:txt复制 * @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@RestController
public class HelloController {代码语言:txt复制 @GetMapping("/hello")代码语言:txt复制 public String hello() {代码语言:txt复制 return "Hello World !";代码语言:txt复制 }代码语言:txt复制}代码语言:txt复制4、创建一个获取登录中的用户信息的接口,用于从请求的Header中直接获取登录用户信息
package cn.gathub.resource.controller;代码语言:txt复制import org.springframework.web.bind.annotation.GetMapping;代码语言:txt复制import org.springframework.web.bind.annotation.RequestMapping;代码语言:txt复制import org.springframework.web.bind.annotation.RestController;代码语言:txt复制import javax.servlet.http.HttpServletRequest;代码语言:txt复制import cn.gathub.resource.domain.User;代码语言:txt复制import cn.hutool.core.convert.Convert;代码语言:txt复制import cn.hutool.json.JSONObject;代码语言:txt复制/**代码语言:txt复制 * 获取登录用户信息接口
*
* @author Honghui [wanghonghui_work@163.com] 2021/3/16
*/
@RestController
@RequestMapping("/user")
public class UserController {代码语言:txt复制 @GetMapping("/currentUser")代码语言:txt复制 public User currentUser(HttpServletRequest request) {代码语言:txt复制 // 从Header中获取用户信息代码语言:txt复制 String userStr = request.getHeader("user");代码语言:txt复制 JSONObject userJsonObject = new JSONObject(userStr);代码语言:txt复制 return User.builder()代码语言:txt复制 .username(userJsonObject.getStr("user_name"))代码语言:txt复制 .id(Convert.toLong(userJsonObject.get("id")))代码语言:txt复制 .roles(Convert.toList(String.class, userJsonObject.get("authorities"))).build();代码语言:txt复制 }代码语言:txt复制}功能演示
在此之前先启动我们的 Nacos 和 Redis
服务,然后依次启动oauth2-auth、oauth2-gateway及oauth2-api服务
我这里测试使用的 Docker 跑的单机版的 Nacos
代码语言:txt复制docker pull nacos/nacos-server代码语言:txt复制docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server1、使用密码模式获取JWT令牌
在这里插入图片描述
2、使用获取到的JWT令牌访问需要权限的接口
在这里插入图片描述
3、使用获取到的JWT令牌访问获取当前登录用户信息的接口,访问地址
在这里插入图片描述
4、当token不存在时
image
5、当JWT令牌过期时,使用refresh_token获取新的JWT令牌
在这里插入图片描述
6、使用授码模式登录时,先访问地址获取授权码:undefined localhost:9201/oauth/authorize?response_type=code&client_id=client- app-2&redirect_uri=重定向地址
7、访问地址,跳转登录页面
image
8、登录成功,进入授权页面
image
9、通过授权,拿到授权码
image
10、拿到授权码,登录
在这里插入图片描述
11、使用没有访问权限的
user账号登录,访问接口时会返回如下信息


