1、概述
SpringCloud Gateway Security oauth2.0 搭建微服务统一认证授权。
项目概述:
环境概述:
- SpringBoot 版本:2.3.1.RELEASE
- SpringCloudAlibaba:2.2.1.RELEASE
技能要求:
需要掌握SpringCloud 、SpringCloudAlibaba 基础使用,了解RBAC、OAuth2.0、JWT。
2、父工程
父工程pom依赖:
<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.0modelVersion>
<groupId>com.roshgroupId>
<artifactId>demoartifactId>
<version>1.0-SNAPSHOTversion>
<modules>
<module>gatewaymodule>
<module>uaamodule>
<module>schoolmodule>
<module>commonmodule>
modules>
<packaging>pompackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.3.1.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR6version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.1.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-collections4artifactId>
<version>4.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.71version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
plugins>
build>
project>
3、uaa认证服务搭建
3.1 pom依赖
<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">
<parent>
<artifactId>demoartifactId>
<groupId>com.roshgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>uaaartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.47version>
dependency>
<dependency>
<groupId>com.roshgroupId>
<artifactId>commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
dependencies>
project>
3.2 application.yml
server:
port: 8500
spring:
application:
name: uaa-server
cloud:
nacos:
discovery:
server-addr: 192.168.226.39:8848
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
global-config:
db-config:
id-type: auto
#暴露监控
management:
endpoints:
web:
exposure:
include: '*'
3.3 Security核心配置
(1) TokenConfig
@Configuration
public class TokenConfig {
/**
* 秘钥串
*/
private static final String SIGNING_KEY = "uaa";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
/**
* 配置令牌管理
*/
@Bean
public AuthorizationServerTokenServices tokenService(ClientDetailsService clientDetailsService,TokenStore tokenStore
,JwtAccessTokenConverter accessTokenConverter) {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService);
service.setSupportRefreshToken(true);
service.setTokenStore(tokenStore);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
return service;
}
/**
* 授权码存储方式
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
return new JdbcAuthorizationCodeServices(dataSource);
}
}
(2) AuthorizationServer
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationServerTokenServices tokenService;
@Autowired
@Qualifier("myClientDetailsService")
private ClientDetailsService clientService;
/**
* 配置客户端详细信息服务
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientService);
}
@Bean("myClientDetailsService")
public ClientDetailsService clientDetailsService(DataSource dataSource, PasswordEncoder passwordEncoder) {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* 令牌访问端点
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenService)
.allowedTokenEndpointRequestMethods(HttpMethod.POST)
.exceptionTranslator(new WebResponseTranslator());
}
/**
* 令牌访问端点安全策略
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
(3) WebSecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SuccessHandler successHandler;
@Autowired
private FailureHandler failureHandler;
@Autowired
private LogoutHandler logoutHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().formLogin()
.loginProcessingUrl("/login").permitAll()
.successHandler(successHandler).permitAll()
.failureHandler(failureHandler).permitAll().and()
.logout().logoutSuccessHandler(logoutHandler).and()
.authorizeRequests()
.antMatchers("/**").permitAll();
}
}
(4) SecurityUserDetailService
@Service
@Slf4j
public class SecurityUserDetailService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) {
UserEntity user = userService.getUserByUsername(username);
if (user == null) {
return null;
}
//获取权限
List permissions = permissionService.getPermissionsByUserId(user.getId());
List codes = permissions.stream().map(PermissionEntry::getCode).collect(Collectors.toList());
String[] authorities = null;
if (CollectionUtils.isNotEmpty(codes)) {
authorities = new String[codes.size()];
codes.toArray(authorities);
}
//身份令牌
String principal = JSON.toJSONString(user);
return User.withUsername(principal).password(user.getPassword()).authorities(authorities).build();
}
}
3.4 启动类
@EnableDiscoveryClient
@SpringBootApplication
public class UaaApplication {
public static void main(String[] args) {
SpringApplication.run(UaaApplication.class);
}
}
4、Gateway服务搭建
4.1 pom
<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">
<parent>
<artifactId>demoartifactId>
<groupId>com.roshgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>gatewayartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.roshgroupId>
<artifactId>commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
dependencies>
project>
4.2 application.yml
server:
port: 9000
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 192.168.226.39:8848
gateway:
routes:
- id: school-server
uri: lb://school-server
predicates:
- Path=/api/school/**
filters:
- RewritePath=/api/school/(?.*),/$\{segment}
- id: uaa-server
uri: lb://uaa-server
predicates:
- Path=/api/uaa/**
filters:
- RewritePath=/api/uaa/(?.*),/$\{segment}
4.3 Security配置
(1) TokenConfig
@Configuration
public class TokenConfig {
/**
* 秘钥串
*/
private static final String SIGNING_KEY = "uaa";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
(2) SecurityConfig
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
.and().csrf().disable().build();
}
}
4.4 过滤器(核心)
@Component
@Slf4j
public class GatewayFilterConfig implements GlobalFilter, Ordered {
@Autowired
private TokenStore tokenStore;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//1 uaa服务所有放行
if (pathMatcher.match("/api/uaa/**", requestUrl)) {
return chain.filter(exchange);
}
//2 检查token是否存在
String token = getToken(exchange);
if (StringUtils.isBlank(token)) {
return noTokenMono(exchange);
}
//3 判断是否是有效的token
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
Map additionalInformation = oAuth2AccessToken.getAdditionalInformation();
//取出用户身份信息
String principal = MapUtils.getString(additionalInformation, "user_name");
//获取用户权限
List authorities = (List) additionalInformation.get("authorities");
JSONObject jsonObject=new JSONObject();
jsonObject.put("principal",principal);
jsonObject.put("authorities",authorities);
//给header里面添加值
String base64 = EncryptUtil.encodeUTF8StringBase64(jsonObject.toJSONString());
ServerHttpRequest tokenRequest = exchange.getRequest().mutate().header("json-token", base64).build();
ServerWebExchange build = exchange.mutate().request(tokenRequest).build();
return chain.filter(build);
} catch (InvalidTokenException e) {
log.info("无效的token: {}", token);
return invalidTokenMono(exchange);
}
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
/**
* 无效的token
*/
private Mono invalidTokenMono(ServerWebExchange exchange) {
JSONObject json = new JSONObject();
json.put("status", HttpStatus.UNAUTHORIZED.value());
json.put("data", "无效的token");
return buildReturnMono(json, exchange);
}
private Mono noTokenMono(ServerWebExchange exchange) {
JSONObject json = new JSONObject();
json.put("status", HttpStatus.UNAUTHORIZED.value());
json.put("data", "没有token");
return buildReturnMono(json, exchange);
}
private Mono buildReturnMono(JSONObject json, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
byte[] bits = json.toJSONString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
4.5 跨域配置
@Configuration
public class GatewayCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
4.6 启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class GatewayApplicaion {
public static void main(String[] args) {
SpringApplication.run(GatewayApplicaion.class);
}
}
5、School微服务
5.1 pom
<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">
<parent>
<artifactId>demoartifactId>
<groupId>com.roshgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>schoolartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.roshgroupId>
<artifactId>commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
5.2 application.yml
server:
port: 8000
spring:
application:
name: school-server
cloud:
nacos:
discovery:
server-addr: 192.168.226.39:8848
5.3 security配置
(1) TokenConfig
@Configuration
public class TokenConfig {
private static final String SIGNING_KEY = "uaa";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
(2) ResouceServerConfig
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
/**
* 资源ID
*/
private static final String RESOURCE_ID = "res1";
/**
* 资源配置
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true)
.accessDeniedHandler(new CustomAccessDeniedHandler());
}
/**
* 请求配置
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
5.4 过滤器(核心)
@Component
public class AuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("json-token");
if (StringUtils.isNotBlank(token)){
String json = EncryptUtil.decodeUTF8StringBase64(token);
JSONObject jsonObject = JSON.parseObject(json);
//获取用户身份信息、权限信息
String principal = jsonObject.getString("principal");
UserEntity user = JSON.parseObject(principal, UserEntity.class);
JSONArray tempJsonArray = jsonObject.getJSONArray("authorities");
String[] authorities = tempJsonArray.toArray(new String[0]);
//身份信息、权限信息填充到用户身份token对象中
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(user,null,
AuthorityUtils.createAuthorityList(authorities));
//创建details
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
//将authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request,response);
}
}
5.5 Controller
(1) 学生接口
@RestController
@RequestMapping("/student")
public class StudentController {
/**
* 老师权限或学生权限
*/
@GetMapping("/grade")
@PreAuthorize("hasAnyAuthority('teacher','student')")
public Object rs(HttpServletRequest request){
Map map=new HashMap<>();
map.put("张三",100);
return map;
}
}
(2) 老师接口
@RestController
@RequestMapping("/teacher")
public class TeacherController {
/**
* 老师权限
*/
@GetMapping("/math/grade")
@PreAuthorize("hasAuthority('teacher')")
public Object rs() {
List
6、集成测试
6.1 账号准备
{"username":"kakaxi","password":"123","authorities":["student","teacher"]}
{"username":"mingren","password":"123","authorities":["student"]}
6.2 启动
6.3 测试
(1) 申请老师权限token
(2) 带着token验证接口
(3) 申请学生token
(4) 验证token
7、Github地址
code、sql脚本
https://github.com/zhurongsheng666/distributed-security