1. 整体架构

文章插图
在这种结构中,网关就是一个资源服务器,它负责统一授权(鉴权)、路由转发、保护下游微服务 。
后端微服务应用完全不用考虑权限问题,也不需要引入spring security依赖,就正常的服务功能开发就行了,不用关注权限 。因为鉴权提到网关去做了 。
网关负责保护它后面的微服务应用,鉴权就是看“问当前资源所需的权限”和“当前用户拥有的权限”之间是否有交集 。
认证服务器负责用户身份的认证校验 。
如此一来,认证服务器就专心做用户身份认证,网关就专心做用户访问授权,业务应用就专心做业务开发,分工明确,各司其职 。
客户端访问服务端接口的过程如下:
1、客户端浏览器(前端)访问后端服务时,网关过滤器发现没有带token,于是返回403未授权
2、客户端重定向到登录页面,用户输入用户名和密码登录后,首先调后端的登录接口,然后获取access_token
3、客户端携带token再次访问服务端接口,网关校验当前用户是否有权限访问该接口
2. 认证服务 (cjs-uaa-server)
pom.xml
<?xml version="1.0" encoding="UTF-8"?><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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.6</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>cjs-uaa-server</artifactId><version>0.0.1-SNAPSHOT</version><name>cjs-uaa-server</name><properties><java.version>1.8</java.version><spring-cloud.version>2020.0.4</spring-cloud.version><alibaba-nacos.version>2021.1</alibaba-nacos.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3.4</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>${alibaba-nacos.version}</version></dependency><dependency><groupId>com.nimbusds</groupId><artifactId>nimbus-jose-jwt</artifactId><version>9.15.2</version></dependency><!--<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId><version>5.5.3</version></dependency>--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>application.ymlserver:port: 8081servlet:context-path: /uaaspring:application:name: cjs-uaa-servercloud:nacos:discovery:server-addr: 192.168.28.32:8848datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/sso?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456redis:host: 192.168.28.31port: 6379password: 123456logging:level:org:springframework:security: debugmybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl默认spring security oauth2生成的token就是一串uuid,里面没有带任何信息,通常会把生成的token存到redis中,这样做的话一般问题也不大,就是费点存储空间而已 。这里我们采用JWT来生成token,JWT的优点就是轻量级、无状态、不用存储,缺点是无法主动撤销 。
有一点需要注意,JWT本身是无状态的,如果我们把JWT又再存到Redis中这就相当于有状态了,JWT+Redis这对JWT而言就相当于是“伤害不大,侮辱性极强” 。唉,没办法,后面要实现退出,我打算这么做了 。
JWT的加密方式有对称加密和非对称加密,这里我们采用非对称加密,接下来用java自带的keytool工具生成密钥 。

文章插图
生成好的jwt.jks文件我们把它放到resources目录下即可
资源服务器在拿到用户传的access_token以后对它进行解密时需要密钥,为此需要写个接口把公钥暴露出去
获取公钥
@Beanpublic KeyPair keyPair() {KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());}再写个Controllerpackage com.example.cjsuaaserver.controller;import com.nimbusds.jose.jwk.JWKSet;import com.nimbusds.jose.jwk.RSAKey;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.security.KeyPair;import java.security.interfaces.RSAPublicKey;import java.util.Map;/** * @Author ChengJianSheng * @Date 2021/11/13 */@RestControllerpublic class KeyPairController {@Autowiredprivate KeyPair keyPair;@GetMapping("/rsa/jwks.json")public Map<String, Object> getKey() {RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic();RSAKey key = new RSAKey.Builder(publicKey).build();return new JWKSet(key).toJSONObject();}}WebSecurityConfig.java package com.example.cjsuaaserver.config;import com.example.cjsuaaserver.service.impl.UserDetailsServiceImpl;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;/** * @Author ChengJianSheng * @Date 2021/11/11 */@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()//.disable()//禁用默认的表单登录.and().authorizeRequests().antMatchers("/rsa/jwks.json", "/oauth/login").permitAll().anyRequest().authenticated().and()//.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)//.and().csrf().disable().cors();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();}}UserDetailsServiceImpl.java package com.example.cjsuaaserver.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.example.cjsuaaserver.domain.UserDetailsDTO;import com.example.cjsuaaserver.entity.SysPermission;import com.example.cjsuaaserver.entity.SysRolePermission;import com.example.cjsuaaserver.entity.SysUser;import com.example.cjsuaaserver.entity.SysUserRole;import com.example.cjsuaaserver.service.ISysPermissionService;import com.example.cjsuaaserver.service.ISysRolePermissionService;import com.example.cjsuaaserver.service.ISysUserRoleService;import com.example.cjsuaaserver.service.ISysUserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;import java.util.List;import java.util.Set;import java.util.stream.Collectors;/** * @Author ChengJianSheng * @Date 2021/11/11 */@Servicepublic class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate ISysUserService sysUserService;@Autowiredprivate ISysUserRoleService sysUserRoleService;@Autowiredprivate ISysRolePermissionService sysRolePermissionService;@Autowiredprivate ISysPermissionService sysPermissionService;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();queryWrapper.eq("username", s);List<SysUser> sysUserList = sysUserService.list();if (null == sysUserList) {throw new UsernameNotFoundException("用户不存在");}SysUser sysUser = sysUserList.get(0);QueryWrapper<SysUserRole> queryWrapper1 = new QueryWrapper<>();queryWrapper1.eq("user_id", sysUser.getId());List<SysUserRole> sysUserRoleList = sysUserRoleService.list(queryWrapper1);List<Integer> roleIds = sysUserRoleList.stream().map(SysUserRole::getRoleId).collect(Collectors.toList());QueryWrapper<SysRolePermission> queryWrapper3 = new QueryWrapper<>();queryWrapper3.in("role_id", roleIds);List<SysRolePermission> sysRolePermissionList = sysRolePermissionService.list(queryWrapper3);List<Integer> permissionIds = sysRolePermissionList.stream().map(SysRolePermission::getPermissionId).collect(Collectors.toList());List<SysPermission> sysPermissionList = sysPermissionService.listByIds(permissionIds);Set<SimpleGrantedAuthority> authorities = sysPermissionList.stream().map(SysPermission::getUrl).map(SimpleGrantedAuthority::new).collect(Collectors.toSet());return new UserDetailsDTO(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), true, authorities);}}接下来,认证服务配置尤为重要,这里面配置了认证客户端和token增强,AuthorizationServerConfig.java package com.example.cjsuaaserver.config;import com.example.cjsuaaserver.domain.UserDetailsDTO;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;import org.springframework.security.oauth2.common.OAuth2AccessToken;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.OAuth2Authentication;import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;import org.springframework.security.oauth2.provider.token.TokenEnhancer;import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;import javax.annotation.Resource;import javax.sql.DataSource;import java.security.KeyPair;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;/** * @Author ChengJianSheng * @Date 2021/11/11 */@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {@Resourceprivate DataSource dataSource;@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.tokenKeyAccess("isAuthenticated()").checkTokenAccess("permitAll()").allowFormAuthenticationForClients();}@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.withClientDetails(new JdbcClientDetailsService(dataSource));}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {List<TokenEnhancer> tokenEnhancerList = new ArrayList<>();tokenEnhancerList.add(jwtTokenEnhancer());tokenEnhancerList.add(jwtAccessTokenConverter());TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(tokenEnhancerList);endpoints.accessTokenConverter(jwtAccessTokenConverter()).tokenEnhancer(tokenEnhancerChain);}public TokenEnhancer jwtTokenEnhancer() {return new TokenEnhancer() {@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {UserDetailsDTO userDetailsDTO = (UserDetailsDTO) authentication.getPrincipal();Map<String, Object> additionalInformation = new HashMap<>();additionalInformation.put("userId", userDetailsDTO.getUserId());additionalInformation.put("username", userDetailsDTO.getUsername());((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(additionalInformation);return accessToken;}};}public JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setKeyPair(keyPair());return jwtAccessTokenConverter;}@Beanpublic KeyPair keyPair() {KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());}}
文章插图
还有最后一个问题,既然是前后端分离,那么就需要我们自己实现登录处理逻辑以及退出
登录逻辑比较简单,利用自带的AuthenticationManager进行认证即可 。由于是授权码模式,所以登录成功以后,需要调用/oauth/token获取access_token,而默认该接口返回的数据结构跟我们自己统一定义的返回结构不一样,为此最简单的方式就是写一个跟它一模一样的Controller这样就覆盖了默认的那个TokenEndpoint里面的/oauth/token
为了实现退出,我们在获取到token以后,将它存到redis中,退出时从redis中将它删除
LoginController.java
package com.example.cjsuaaserver.controller;import com.example.cjsuaaserver.domain.LoginDTO;import com.example.cjsuaaserver.domain.RespResult;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.Authentication;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.oauth2.common.OAuth2AccessToken;import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;import org.springframework.web.HttpRequestMethodNotSupportedException;import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.security.Principal;import java.util.Map;import java.util.concurrent.TimeUnit;/** * @Author ChengJianSheng * @Date 2021/11/17 */@RestController@RequestMapping("/oauth")public class LoginController {@Autowiredprivate TokenEndpoint tokenEndpoint;@Resourceprivate AuthenticationManager authenticationManager;@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 获取Token* 这里取巧直接定义了一个跟TokenEndpoint里面一样的请求路径,这样的话自动的就被覆盖了 。请求通过我们自定义的这个方法,就可以对请求结构进行封装,按照我们想要的格式返回了 。* 多次实验我发现,一定有过滤器会对/oauth/token进行拦截处理,不然第一个参数principal根本就不会有值,这里的principal代表的是oauth2客户端* 如果自己随便定义一个不叫/oauth/token的话,请求的时候又不知道怎么传参 o(╥﹏╥)o* 网上看到还有一种方式是自己定义一个aop去拦截这个请求,并修改返回数据格式*/@PostMapping("/token")public RespResult getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal, parameters).getBody();//放入Redis中,为了实现退出登录String jti = (String) oAuth2AccessToken.getAdditionalInformation().get("jti");int expiresIn = oAuth2AccessToken.getExpiresIn();String key = "TOKEN:" + jti;String value = https://tazarkount.com/read/oAuth2AccessToken.getValue();stringRedisTemplate.opsForValue().set(key, value, expiresIn, TimeUnit.SECONDS);return new RespResult(200,"success", true, oAuth2AccessToken);}/*** 登录* @param dto* @return*/@PostMapping("/login")public RespResult login(@RequestBody LoginDTO dto) {//校验验证码//登录UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword());Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);SecurityContextHolder.getContext().setAuthentication(authentication);Object principal = authentication.getPrincipal();System.out.println(principal);return new RespResult(200, "success", true, principal);}/*** 退出(清除缓存)*/@GetMapping("/logout")public RespResult logout(HttpServletRequest request) {String userStr = request.getHeader("user");//解析出jtiString jti = "xxx";//清除缓存stringRedisTemplate.delete("TOKEN:" + jti);return null;}}3. 业务应用服务 (cjs-order-server)pom.xml
<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><version>${alibaba-nacos.version}</version></dependency>application.yml server:port: 8082servlet:context-path: /orderspring:application:name: cjs-order-servercloud:nacos:discovery:server-addr: 192.168.28.32:8848OrderController.java package com.example.cjsorderserver.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;/** * @Author ChengJianSheng * @Date 2021/11/16 */@RestController@RequestMapping("/order")public class OrderController {@GetMapping("/pageList")public String pageList() {return "good";}}4. 网关 (cjs-gateway-server)网关服务特别重要
pom.xml
<?xml version="1.0" encoding="UTF-8"?><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 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.6</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.example</groupId><artifactId>cjs-gateway-server</artifactId><version>0.0.1-SNAPSHOT</version><name>cjs-gateway-server</name><properties><java.version>1.8</java.version><spring-cloud.version>2020.0.4</spring-cloud.version><alibaba-nacos.version>2021.1</alibaba-nacos.version><spring-security-oauth2.version>5.6.0</spring-security-oauth2.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>${alibaba-nacos.version}</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId><version>${spring-security-oauth2.version}</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId><version>${spring-security-oauth2.version}</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.12.0</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.78</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>application.yml server:port: 8080spring:application:name: cjs-gateway-servercloud:nacos:discovery:server-addr: 192.168.28.32:8848gateway:routes:- id: oauth2-authuri: lb://cjs-uaa-serverpredicates:- Path=/uaa/**- id: order-serveruri: lb://cjs-order-serverpredicates:- Path=/order/**discovery:locator:enabled: truelower-case-service-id: truesecurity:oauth2:resourceserver:jwt:jwk-set-uri: http://localhost:8081/uaa/rsa/jwks.jsonredis:host: 192.168.28.31port: 6379password: 123456secure:ignore:urls:- /order/hello/sayHello- /uaa/**logging:level:org.springframework.cloud.gateway: debug作为资源服务器这样一个角色,自然少不了资源服务器配置ResourceServerConfig.java
package com.example.gateway.config;import com.example.gateway.handler.CustomAuthorizationManager;import com.example.gateway.handler.CustomServerAccessDeniedHandler;import com.example.gateway.handler.CustomServerAuthenticationEntryPoint;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.convert.converter.Converter;import org.springframework.security.authentication.AbstractAuthenticationToken;import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;import org.springframework.security.config.web.server.ServerHttpSecurity;import org.springframework.security.oauth2.jwt.Jwt;import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;import org.springframework.security.web.server.SecurityWebFilterChain;import reactor.core.publisher.Mono;/** * @Author ChengJianSheng * @Date 2021/11/14 */@Configuration@EnableWebFluxSecuritypublic class ResourceServerConfig {@Autowiredprivate IgnoreUrlsConfig ignoreUrlsConfig;@Autowiredprivate CustomAuthorizationManager customAuthorizationManager;@Autowiredprivate CustomServerAccessDeniedHandler customServerAccessDeniedHandler;@Autowiredprivate CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;@Beanpublic SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());http.authorizeExchange().pathMatchers(ignoreUrlsConfig.getUrls()).permitAll().anyExchange().access(customAuthorizationManager).and().exceptionHandling().accessDeniedHandler(customServerAccessDeniedHandler).authenticationEntryPoint(customServerAuthenticationEntryPoint).and().csrf().disable();return http.build();}public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);}}IgnoreUrlsConfig.java 是用来配置不需要授权的url的 package com.example.gateway.config;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;/** * @Author ChengJianSheng * @Date 2021/11/15 */@Data@Component@ConfigurationProperties(prefix = "secure.ignore")public class IgnoreUrlsConfig {private String[] urls;}利用ReactiveAuthorizationManager实现授权逻辑CustomAuthorizationManager.java
package com.example.gateway.handler;import com.example.gateway.constant.AuthConstants;import org.apache.commons.lang3.StringUtils;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.security.authorization.AuthorizationDecision;import org.springframework.security.authorization.ReactiveAuthorizationManager;import org.springframework.security.core.Authentication;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.web.server.authorization.AuthorizationContext;import org.springframework.stereotype.Component;import reactor.core.publisher.Mono;import java.util.ArrayList;import java.util.List;/** * @Author ChengJianSheng * @Date 2021/11/15 */@Componentpublic class CustomAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {@Overridepublic Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext context) {ServerHttpRequest request = context.getExchange().getRequest();String path = request.getURI().getPath();String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER);if (StringUtils.isBlank(token)) {return Mono.just(new AuthorizationDecision(false));}List<String> authorities = new ArrayList<>();authorities.add(AuthConstants.ROLE_PREFIX + path);return authentication.filter(Authentication::isAuthenticated).flatMapIterable(Authentication::getAuthorities).map(GrantedAuthority::getAuthority)//.any(authorities::contains).any(roleId->{System.out.println(path);System.out.println(roleId);System.out.println(authorities);return authorities.contains(roleId);}).map(AuthorizationDecision::new).defaultIfEmpty(new AuthorizationDecision(false));}} 
文章插图
这里的资源就是url,就是请求路径,资源所需的权限可以从数据库或者缓存Redis中查,也可以提前将所有资源与权限的对应关系存到Redis中,这里直接去取
CustomServerAccessDeniedHandler.java
package com.example.gateway.handler;import com.alibaba.fastjson.JSON;import com.example.gateway.domain.Result;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.security.access.AccessDeniedException;import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.nio.charset.Charset;/** * @Author ChengJianSheng * @Date 2021/11/15 */@Componentpublic class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {@Overridepublic Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.OK);response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);String body = JSON.toJSONString(new Result("拒绝访问"));DataBuffer buffer =response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));return exchange.getResponse().writeWith(Mono.just(buffer));}}CustomServerAuthenticationEntryPoint.java package com.example.gateway.handler;import com.alibaba.fastjson.JSON;import com.example.gateway.domain.Result;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.security.core.AuthenticationException;import org.springframework.security.web.server.ServerAuthenticationEntryPoint;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.nio.charset.Charset;/** * @Author ChengJianSheng * @Date 2021/11/15 */@Componentpublic class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {@Overridepublic Mono<Void> commence(ServerWebExchange exchange, AuthenticationException ex) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.OK);response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);String body = JSON.toJSONString(new Result("未认证"));DataBuffer buffer =response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));return response.writeWith(Mono.just(buffer));}}最后,为了避免业务应用自己还要去解析token,网关在将授权通过的请求转发给下游业务应用时,应该提前将token解析好,并放到请求头中,这样业务应用直接从请求头中获取用户信息 CustomGlobalFilter.java
package com.example.gateway.filter;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.example.gateway.constant.AuthConstants;import com.example.gateway.domain.Result;import com.nimbusds.jose.JWSObject;import org.apache.commons.lang3.StringUtils;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.core.io.buffer.DataBuffer;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.nio.charset.Charset;import java.text.ParseException;/** * @Author ChengJianSheng * @Date 2021/11/17 */@Componentpublic class CustomGlobalFilter implements GlobalFilter, Ordered {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String token = exchange.getRequest().getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER);if (StringUtils.isBlank(token)) {return chain.filter(exchange);}String realToken = token.replace(AuthConstants.JWT_TOKEN_PREFIX, "");try {JWSObject jwsObject = JWSObject.parse(realToken);String payload = jwsObject.getPayload().toString();JSONObject jsonObject = JSON.parseObject(payload);String jti = jsonObject.getString("jti");boolean flag = stringRedisTemplate.hasKey(AuthConstants.TOKEN_WHITELIST_PREFIX + jti);if (!flag) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.OK);response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);response.getHeaders().set("Access-Control-Allow-Origin", "*");response.getHeaders().set("Cache-Control", "no-cache");String body = JSON.toJSONString(new Result("无效的Token"));DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));return response.writeWith(Mono.just(buffer));}ServerHttpRequest request = exchange.getRequest().mutate().header("user", payload).build();exchange = exchange.mutate().request(request).build();} catch (ParseException e) {e.printStackTrace();}return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;}}5. 演示
文章插图

文章插图
这里为了演示没有禁用默认的表单登录,正式开发的时候最后禁用默认登录,改用自定义登录页面

文章插图

文章插图

文章插图
登录

文章插图
授权

文章插图
这里由于重定向到百度,在postman中不好看返回的code,我们用Wireshark看

文章插图
通过Wireshark可以看到,确实重定向了
然后获取token

文章插图
检查token

文章插图

文章插图

文章插图
携带access_token访问业务接口,比如 http://localhost:8082/order/order/pageList 可以看到请求header中确实带了已经解析好的用户信息 ,那是我们在前面CustomGlobalFilter.java中放的

文章插图
最后,补充一点:业务应用中利用利用过滤器将header中的用户信息放到ThreadLocal变量中,一遍后续方法中可以直接从上下文中获取
UserInfo.java
package com.cjs.component.user.domain;import lombok.Data;import java.io.Serializable;/** * @Author ChengJianSheng * @Date 2021/12/1 */@Datapublic class UserInfo implements Serializable {private Long userId;private String username;private String mobile;}UserInfoContext.javapackage com.tgf.component.user.service;import com.tgf.component.user.domain.UserInfo;/** * @Author ChengJianSheng * @Date 2021/12/1 */public class UserInfoContext {public static final String HEADER_USER_INFO = "X-USERINFO";private static ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();public static UserInfo get() {return threadLocal.get();}public static void set(UserInfo userInfo) {threadLocal.set(userInfo);}}UserInfoFilter.java package com.cjs.component.user.filter;import com.alibaba.fastjson.JSON;import com.tgf.component.user.domain.UserInfo;import com.tgf.component.user.service.UserInfoContext;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.springframework.core.annotation.Order;import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;/** * @Author ChengJianSheng * @Date 2021/12/1 */@Slf4j@Order(1)@Componentpublic class UserInfoFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {String userInfoJson = ((HttpServletRequest) request).getHeader(UserInfoContext.HEADER_USER_INFO);if (StringUtils.isNotBlank(userInfoJson)) {UserInfo userInfo = JSON.parseObject(userInfoJson, UserInfo.class);UserInfoContext.set(userInfo);}chain.doFilter(request, response);}} 参考文档
https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/html5/#boot-features-security-oauth2-single-sign-on
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.security.oauth2.server
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webflux-oauth2resourceserver-jwt-authorization
【微服务和前后端 微服务下前后端分离的统一认证授权服务,基于Spring Security OAuth2 + Spring Cloud Gateway实现单点登录】
- 春季老年人吃什么养肝?土豆、米饭换着吃
- 三八妇女节节日祝福分享 三八妇女节节日语录
- 老人谨慎!选好你的“第三只脚”
- 校方进行了深刻的反思 青岛一大学生坠亡校方整改校规
- 脸皮厚的人长寿!有这特征的老人最长寿
- 长寿秘诀:记住这10大妙招 100%增寿
- 春季老年人心血管病高发 3条保命要诀
- 眼睛花不花要看四十八 老年人怎样延缓老花眼
- 香槟然能防治老年痴呆症? 一天三杯它人到90不痴呆
- 老人手抖的原因 为什么老人手会抖
