一、涉及的技术(绿色背景)
二、基本流程
三、构建项目(基础环境搭建请参考微服务架构-简介_Morik的博客-CSDN博客)
1、构建聚合项目
1.1、打开idea新建一个springBoot项目(注意alibabaCloud、springCloud、springBoot的版本匹配)详细版本请参考官网:版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub
2、构建公共项目
2.1、idea-->new model-->maven
2.2、pom文件中可以放一些公共的包、后期可以放一些工具类在里边
3、构建资源项目(影视资讯业务)
3.2、同样的方法新建一个model、然后在pom中加入两个核心依赖包
3.3、启动类注册为资源服务器(@EnableResourceServer)
3.5、编写业务接口(通过@AuthenticationPrincipal注解获取token的用户信息)
4、构建网关项目(注意:RestTemplate要手动增强,要保证在Getaway启动完成之前通过负载均衡提前把授权服务器上的公钥拿到。如果在Getaway启动完成后批量的订单请求打过来LB还没初始化完成所有请求没公钥无法鉴权全部作废)
4.1、新建model并在pom中新增核心依赖
umf
cn.morik
0.0.1-SNAPSHOT
4.0.0
cn.morik.umf
route
网关(鉴权、流控)
org.springframework.boot
spring-boot-starter-webflux
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-sentinel
com.alibaba.cloud
spring-cloud-alibaba-sentinel-gateway
com.alibaba.csp
sentinel-datasource-nacos
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
io.jsonwebtoken
jjwt-api
0.10.5
io.jsonwebtoken
jjwt-impl
0.10.5
runtime
io.jsonwebtoken
jjwt-jackson
0.10.5
runtime
com.github.xiaoymin
knife4j-spring-boot-starter
2.0.3
org.springframework.boot
spring-boot-configuration-processor
true
org.projectlombok
lombok
4.2、yml新增主配置和跨域配置
package cn.morik.umf.route.filter;
import cn.morik.umf.route.exception.GateWayException;
import cn.morik.umf.route.utils.MDA;
import cn.morik.umf.route.utils.MorikRestTemplate;
import cn.morik.umf.route.utils.SystemErrorType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 认证过滤器,根据url判断用户请求是要经过认证 才能访问
*/
@Component
@Slf4j
public class AuthorizationFilter implements GlobalFilter, Ordered, InitializingBean {
@Autowired
private MorikRestTemplate restTemplate;
private PublicKey publicKey;
private AntPathMatcher matcher = new AntPathMatcher();
/**
* 请求各个微服务 不需要用户认证的URL
*/
private static Set shouldSkipUrl = new LinkedHashSet<>();
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String reqPath = exchange.getRequest().getURI().getPath();
log.info("网关认证开始URL->:{}", reqPath);
//1:不需要认证的url
if (shouldSkip(reqPath)) {
log.info("无需认证的路径");
return chain.filter(exchange);
}
//获取请求头
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
//请求头为空
if (StringUtils.isEmpty(authHeader)) {
log.warn("需要认证的url,请求头为空");
throw new GateWayException(SystemErrorType.UNAUTHORIZED_HEADER_IS_EMPTY);
}
//交易我们的jwt 若jwt不对或者超时都会抛出异常
Claims claims = validateJwtToken(authHeader);
//向headers中放文件,记得build
ServerHttpRequest request = exchange.getRequest().mutate().header("username", claims.get("user_name").toString()).build();
//将现在的request 变成 change对象
ServerWebExchange serverWebExchange = exchange.mutate().request(request).build();
//从jwt中解析出权限集合进行判断
hasPremisson(claims, reqPath);
return chain.filter(serverWebExchange);
}
private Claims validateJwtToken(String authHeader) {
String token = null;
try {
token = StringUtils.substringAfter(authHeader, "bearer ");
Jwt parseClaimsJwt = Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
Claims claims = parseClaimsJwt.getBody();
log.info("claims:{}", claims);
return claims;
} catch (Exception e) {
log.error("校验token异常:{},异常信息:{}", token, e.getMessage());
throw new GateWayException(SystemErrorType.INVALID_TOKEN);
}
}
private boolean hasPremisson(Claims claims, String currentUrl) {
boolean hasPremisson = false;
//登陆用户的权限集合判断
List premessionList = claims.get("authorities", List.class);
for (String url : premessionList) {
if (matcher.match(url, currentUrl)) {
hasPremisson = true;
break;
}
}
if (!hasPremisson) {
log.warn("权限不足");
throw new GateWayException(SystemErrorType.FORBIDDEN);
}
return hasPremisson;
}
private boolean shouldSkip(String reqPath) {
for (String skipPath : shouldSkipUrl) {
if (matcher.match(skipPath, reqPath)) {
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
@Override
public void afterPropertiesSet() throws Exception {
/**
*实际上,这边需要通过去数据库读取 不需要认证的URL,不需要认证的URL是各个微服务
* 开发模块的人员提供出来的. 我在这里没有去查询数据库了,直接模拟写死
*/
shouldSkipUrl.add("/oauth/token");
shouldSkipUrl.add("/oauth/check_token");
shouldSkipUrl.add("/user/getCurrentUser");
// shouldSkipUrl.add("/film/api/info");
//初始化公钥
this.publicKey = genPublicKeyByTokenKey();
}
/**
* 方法实现说明:去认证服务器上获取tokenKey
*/
private String getTokenKey() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(MDA.clientId, MDA.clientSecret);
HttpEntity> entity = new HttpEntity<>(null, headers);
try {
ResponseEntity
5、构建授权中心
5.1、新建model并导入核心pom依赖
umf
cn.morik
0.0.1-SNAPSHOT
4.0.0
cn.morik.umf
oauth
授权中心
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-oauth2
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jdbc
com.alibaba
druid
1.1.8
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
com.nimbusds
nimbus-jose-jwt
RELEASE
org.projectlombok
lombok
5.2、yml配置
server:
port: 8888
spring:
application:
name: oauth-server
cloud:
nacos:
discovery:
server-addr: 192.168.1.9:8848
datasource:
username: root
password: Morik1234567890.
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.9:3306/morik-user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false
type: com.alibaba.druid.pool.DruidDataSource
# redis:
# host: 192.168.1.9
# port: 6379
# password: root
# session:
# store-type: redis
# timeout: 1800
logging:
level:
cn:
morik:
umf:
oauth:
config:
role:
mapper: debug
##A.CTable配置
#mybatis:
# #自动更新表
# table:
# auto: update
# #实体类扫描地址
# model:
# pack: cn.morik.umf.oauth.config.role.entity
# #数据库类型
# database:
# type: mysql
5.3、授权配置
package cn.morik.umf.oauth.indb;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
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 org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import javax.sql.DataSource;
import java.security.KeyPair;
import java.util.Arrays;
@Configuration
@EnableAuthorizationServer
public class AuthServerInDbConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
@Autowired
private UMFUserDetailService userDetailsService;
/**
* 方法实现说明: 使用jwt存储token,我们创建jwtTOkenStore的时候 需要一个组件
* jwtAccessTokenConverter 所以我们 可以通过@Bean的形式 创建一个该组件.
*/
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 这个组件 用于jwt basecode 字符串和 安全认证对象的信息转化
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//jwt的密钥(用来保证jwt 字符串的安全性 jwt可以防止篡改 但是不能防窃听 所以jwt不要 放敏感信息)
converter.setKeyPair(keyPair());
//converter.setSigningKey("123456");
return converter;
}
/**
* KeyPair是 非对称加密的公钥和私钥的保存者
* @return
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}
/**
* 该组件就是用来给jwt令牌中添加额外信息的 来增强我们的jwt的令牌信息
* @return
*/
@Bean
public UMFTokenEnhancer umfTokenEnhancer() {
return new UMFTokenEnhancer();
}
/**
* 方法实现说明:认证服务器能够给哪些 客户端颁发token 我们需要把客户端的配置 存储到
* 数据库中 可以基于内存存储和db存储
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
/**
* 方法实现说明:用于查找我们第三方客户端的组件 主要用于查找 数据库表 oauth_client_details
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 方法实现说明:授权服务器的配置的配置
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/*
增加我们的令牌信息
*/
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(umfTokenEnhancer(),jwtAccessTokenConverter()));
endpoints.tokenStore(tokenStore()) //授权服务器颁发的token 怎么存储的
.tokenEnhancer(tokenEnhancerChain)
.userDetailsService(userDetailsService) //用户来获取token的时候需要 进行账号密码
.authenticationManager(authenticationManager);
}
/**
* 方法实现说明:授权服务器安全配置
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//第三方客户端校验token需要带入 clientId 和clientSecret来校验
security .checkTokenAccess("isAuthenticated()")
.tokenKeyAccess("isAuthenticated()");//来获取我们的tokenKey需要带入clientId,clientSecret
security.allowFormAuthenticationForClients();
}
}
四、效果展示
1、获取token
2、通过Getaway获取影视资讯服务信息