千家信息网

Spring gateway和Oauth2如何实现单点登录

发表于:2025-02-08 作者:千家信息网编辑
千家信息网最后更新 2025年02月08日,这篇文章主要介绍Spring gateway和Oauth2如何实现单点登录,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!场景:按职能,鉴权系统需要划分 网关(spring ga
千家信息网最后更新 2025年02月08日Spring gateway和Oauth2如何实现单点登录

这篇文章主要介绍Spring gateway和Oauth2如何实现单点登录,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!

场景:

按职能,鉴权系统需要划分 网关(spring gateway) + 鉴权(auth-server)。本文通过实践搭建鉴权系统。

spring gateway

首先引入pom依赖

1、resilience 熔断器
2、gateway 网关
3、eureka client 服务注册中心
4、lombok插件
5、actuator状态监控

                     io.github.resilience4j            resilience4j-feign            1.1.0                            org.springframework.boot            spring-boot-starter                            org.springframework.boot            spring-boot-starter-test            test                                    org.springframework.cloud            spring-cloud-starter-gateway                                    org.springframework.cloud            spring-cloud-starter-netflix-eureka-client            2.2.2.RELEASE                                            org.projectlombok            lombok            true                            org.springframework.boot            spring-boot-starter-actuator        

application.yml配置

1、gateway信息
2、actuator状态信息
3、配置eureka server地址及注册信息
4、日志配置
5、获取oauth3的jwt key

server:  port: 18890spring:  application:    name: open-api-gateway  profiles:    active: local  cloud:    gateway:      discovery:        locator:          enabled: true          lower-case-service-id: true      globalcors:        corsConfigurations:          '[/**]':            allowedOrigins: "*"            allowedMethods: "*"      default-filters:        - AddRequestParameter=gateway_type, member        - AddRequestHeader=gateway_type, membermanagement:  endpoints:    web:      exposure:        include: "*"  endpoint:    health:      show-details: always      eureka:  instance:    prefer-ip-address: true    instance-id: ${spring.cloud.client.ip-address}:${server.port}  client:    service-url:      defaultZone: http://127.0.0.1:22001/eurekalogging:  level:    com.dq.edu: debug    com:      netflix:        discovery: errorspring:  security:    oauth3:      resourceserver:        jwt:          jwk-set-uri: http://127.0.0.1:18889/auth-server/private/jwk_public_key

gateway 项目目录

核心内容:security配置、PermissionFilter鉴权过滤器

1、security配置

package com.digquant.openapigateway.config;import com.digquant.openapigateway.entity.Response;import lombok.AllArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.core.io.buffer.DataBufferFactory;import org.springframework.core.io.buffer.DataBufferUtils;import org.springframework.http.HttpMethod;import org.springframework.http.MediaType;import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;import org.springframework.security.config.web.server.ServerHttpSecurity;import org.springframework.security.oauth3.jwt.ReactiveJwtDecoder;import org.springframework.security.oauth3.server.resource.web.server.ServerBearerTokenAuthenticationConverter;import org.springframework.security.web.server.SecurityWebFilterChain;import org.springframework.security.web.server.ServerAuthenticationEntryPoint;import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;import org.springframework.util.StringUtils;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.nio.charset.Charset;@Slf4j@Configuration@AllArgsConstructor@EnableWebFluxSecuritypublic class SecurityConfig {    private final ReactiveJwtDecoder jwtDecoder;    @Bean    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {        http.cors().disable().csrf().disable();        http                .authorizeExchange()                .pathMatchers("/**").permitAll()                .pathMatchers("/**/public/**").permitAll()                .pathMatchers("/**/static/**").permitAll()                .pathMatchers("/*/oauth/**").permitAll()                .pathMatchers("/actuator/**").permitAll()                .pathMatchers(HttpMethod.OPTIONS).permitAll()                .anyExchange().authenticated()                .and()                .exceptionHandling()                .accessDeniedHandler(serverAccessDeniedHandler())                .authenticationEntryPoint(serverAuthenticationEntryPoint())                .and()                .oauth3ResourceServer()                .jwt()                .jwtDecoder(jwtDecoder)                .and()                .bearerTokenConverter(new ServerBearerTokenAuthenticationConverter());        return http.build();    }    @Bean    public ServerAccessDeniedHandler serverAccessDeniedHandler() {        return (exchange, denied) -> {            log.debug("没有权限");            String errMsg = StringUtils.hasText(denied.getMessage()) ? denied.getMessage() : "没有权限";            Response result = new Response(1, errMsg);            return create(exchange, result);        };    }    @Bean    public ServerAuthenticationEntryPoint serverAuthenticationEntryPoint() {        return (exchange, e) -> {            log.debug("认证失败");            String errMsg = StringUtils.hasText(e.getMessage()) ? e.getMessage() : "认证失败";            Response result = new Response(1, errMsg);            return create(exchange, result);        };    }    private Mono create(ServerWebExchange exchange, Response result) {        return Mono.defer(() -> Mono.just(exchange.getResponse()))                .flatMap(response -> {                    response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);                    DataBufferFactory dataBufferFactory = response.bufferFactory();                    DataBuffer buffer = dataBufferFactory.wrap(createErrorMsg(result));                    return response.writeWith(Mono.just(buffer))                            .doOnError(error -> DataBufferUtils.release(buffer));                });    }    private byte[] createErrorMsg(Response result) {        return result.getErrMsg().getBytes(Charset.defaultCharset());    }}

总结:

gateway是基于 WebFlux的响应式编程框架,所以在使用securityConfig时采用的注解是@EnableWebFluxSecurity

2、PermissionFilter

package com.digquant.openapigateway.filter;import com.digquant.openapigateway.utils.IStrings;import lombok.AllArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.core.annotation.Order;import org.springframework.http.HttpHeaders;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.security.core.context.ReactiveSecurityContextHolder;import org.springframework.security.core.context.SecurityContext;import org.springframework.security.oauth3.jwt.Jwt;import org.springframework.security.oauth3.server.resource.authentication.AbstractOAuth3TokenAuthenticationToken;import org.springframework.security.oauth3.server.resource.authentication.JwtAuthenticationToken;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import org.springframework.web.server.WebFilter;import org.springframework.web.server.WebFilterChain;import reactor.core.publisher.Mono;import java.util.Objects;@Slf4j@Order@Component@AllArgsConstructorpublic class PermissionFilter implements WebFilter {    @Override    public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {        return ReactiveSecurityContextHolder.getContext()                //排除没有Token的                .filter(Objects::nonNull)//                //检查该路径是否需要权限//                .filter(var -> permissionStore.usingPermission(exchange.getRequest().getPath().value()))                .map(SecurityContext::getAuthentication)                .map(authentication -> (JwtAuthenticationToken) authentication)                .doOnNext(jwtAuthenticationToken -> {                    String path = exchange.getRequest().getPath().value();                    log.info("请求 uri {}", path);                })                .map(AbstractOAuth3TokenAuthenticationToken::getPrincipal)                .map(var -> (Jwt) var)                .map(jwt -> {                    String tokenValue = jwt.getTokenValue();                    ServerHttpRequest.Builder builder = exchange.getRequest().mutate();                    builder.header(HttpHeaders.AUTHORIZATION, IStrings.splice("Bearer ", tokenValue));                    ServerHttpRequest request = builder.build();                    return exchange.mutate().request(request).build();                })                .defaultIfEmpty(exchange)                .flatMap(chain::filter);    }}

总结
1、使用permissionStore来记录uri的权限要求
2、获取到jwtToken时,处理token所携带的权限,用于匹配是否能请求对应资源

OAuth3 项目目录

pom依赖

1、eureka client
2、spring boot mvc
3、redis 用于存储jwt
4、mysql用于记录用户资源权限
5、oauth3组件
6、httpclient fregn用于用户登陆鉴权

                     org.springframework.cloud            spring-cloud-starter-netflix-eureka-client            2.2.2.RELEASE                            org.springframework.boot            spring-boot-starter-web                            org.springframework.boot            spring-boot-starter-data-redis                            org.mybatis.spring.boot            mybatis-spring-boot-starter            2.1.1                           com.alibaba            druid            1.1.21                            mysql            mysql-connector-java            8.0.18                                 org.springframework.cloud            spring-cloud-starter-oauth3            2.2.0.RELEASE                            org.springframework.cloud            spring-cloud-starter-security            2.2.0.RELEASE                            org.springframework.security            spring-security-oauth3-jose            5.2.2.RELEASE                             org.springframework.cloud            spring-cloud-starter-openfeign            2.2.2.RELEASE                            io.github.openfeign            feign-okhttp            10.9                            org.projectlombok            lombok            true            

应用配置

server:  port: 37766spring:  application:    name: auth-server  mvc:    throw-exception-if-no-handler-found: true  profiles:    active: devmybatis:  mapper-locations: classpath:mapper/*.xml  type-aliases-package: com.digquant.enity.pologging:  level:    com:      digquant:        dao: info  file:    path: /dq/log/new/auth-serverdigquant:  authorization:    auth-jwt-jks: hq-jwt.jks    auth-jwt-key: hq-jwt    auth-jwt-password: hq940313    access-token-validity-seconds: 14400    refresh-token-validity-seconds: 86400

1、AuthorizationServerConfig配置

package com.digquant.config;import com.digquant.dao.CustomRedisTokenStore;import com.digquant.enity.JWTProperties;import com.digquant.enity.Response;import com.digquant.service.OAuthUserService;import lombok.AllArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.ClassPathResource;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.oauth3.common.exceptions.OAuth3Exception;import org.springframework.security.oauth3.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth3.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth3.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth3.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth3.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;import org.springframework.security.oauth3.provider.ClientDetailsService;import org.springframework.security.oauth3.provider.client.JdbcClientDetailsService;import org.springframework.security.oauth3.provider.error.WebResponseExceptionTranslator;import org.springframework.security.oauth3.provider.token.DefaultTokenServices;import org.springframework.security.oauth3.provider.token.TokenStore;import org.springframework.security.oauth3.provider.token.store.JwtAccessTokenConverter;import org.springframework.security.oauth3.provider.token.store.KeyStoreKeyFactory;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.security.web.access.AccessDeniedHandler;import javax.servlet.http.HttpServletResponse;import javax.sql.DataSource;import java.io.IOException;@Slf4j@Configuration@AllArgsConstructor@EnableAuthorizationServerpublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    private final JWTProperties jwtProperties;    /**     * 注入权限验证控制器 支持 password grant type     */    private final AuthenticationManager authenticationManager;    /**     * 数据源     */    private final DataSource dataSource;    /**     * 开启refresh_token     */    private final OAuthUserService userService;    /**     * 采用redis 存储token     */    private final RedisConnectionFactory redisConnectionFactory;    @Override    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {        security.allowFormAuthenticationForClients()                .checkTokenAccess("permitAll()")                .tokenKeyAccess("permitAll()")                .authenticationEntryPoint(authenticationEntryPoint())                .accessDeniedHandler(accessDeniedHandler());    }    @Bean    public AccessDeniedHandler accessDeniedHandler() {        return (request, response, accessDeniedException) -> {            Response result = new Response(1, accessDeniedException.getMessage());            writerResponse(response, result, HttpStatus.FORBIDDEN.value());        };    }    @Bean    public AuthenticationEntryPoint authenticationEntryPoint() {        return (request, response, authException) -> {            Response result = new Response(1, authException.getMessage());            writerResponse(response, result, HttpStatus.UNAUTHORIZED.value());        };    }    private void writerResponse(HttpServletResponse response, Response result, int status) throws IOException {        response.setStatus(status);        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);        response.setCharacterEncoding("UTF-8");        response.getWriter().print(result.getErrMsg());        response.getWriter().flush();    }    @Bean("redisTokenStore")    public TokenStore redisTokenStore() {        return new CustomRedisTokenStore(redisConnectionFactory);    }    @Bean    public JwtAccessTokenConverter jwtAccessTokenConverter() {        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();        jwtAccessTokenConverter.setKeyPair(keyStoreKeyFactory().getKeyPair(jwtProperties.getAuthJwtKey()));        return jwtAccessTokenConverter;    }    @Bean    public KeyStoreKeyFactory keyStoreKeyFactory() {        return new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getAuthJwtJks()), jwtProperties.getAuthJwtPassword().toCharArray());    }    @Bean    public DefaultTokenServices tokenServices() {        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();        defaultTokenServices.setTokenStore(redisTokenStore());        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());        defaultTokenServices.setSupportRefreshToken(true);        defaultTokenServices.setReuseRefreshToken(false);        defaultTokenServices.setAccessTokenValiditySeconds(jwtProperties.getAccessTokenValiditySeconds());        defaultTokenServices.setRefreshTokenValiditySeconds(jwtProperties.getRefreshTokenValiditySeconds());        return defaultTokenServices;    }    @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        //开启密码授权类型        endpoints                .authenticationManager(authenticationManager)                //配置token存储方式                .tokenStore(redisTokenStore())                //需要额外配置,用于refres_token                .userDetailsService(userService)                //                .tokenServices(tokenServices())                .accessTokenConverter(jwtAccessTokenConverter())                .exceptionTranslator(exceptionTranslator());    }    @Bean    public WebResponseExceptionTranslator exceptionTranslator() {        return exception -> {            return ResponseEntity.status(HttpStatus.OK).body(new OAuth3Exception(exception.getMessage()));        };    }    @Bean    public ClientDetailsService clientDetails() {        return new JdbcClientDetailsService(dataSource);    }    @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {//        clients.withClientDetails(clientDetails());        clients.inMemory()                .withClient("open_api")                .authorizedGrantTypes("password","refresh_token")                .authorities("USER")                .scopes("read", "write")                .resourceIds("auth-server")                .secret(new BCryptPasswordEncoder().encode("digquant"));    }}

2、ResourceServerConfig 资源服务配置

package com.digquant.config;import lombok.AllArgsConstructor;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.http.HttpMethod;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.oauth3.config.annotation.web.configuration.EnableResourceServer;import org.springframework.security.oauth3.config.annotation.web.configuration.ResourceServerConfigurerAdapter;import org.springframework.security.oauth3.config.annotation.web.configurers.ResourceServerSecurityConfigurer;import org.springframework.security.web.AuthenticationEntryPoint;import org.springframework.security.web.access.AccessDeniedHandler;@Order(6)@Configuration@AllArgsConstructor@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter {    private final AccessDeniedHandler accessDeniedHandler;    private final AuthenticationEntryPoint authenticationEntryPoint;    @Override    public void configure(HttpSecurity http) throws Exception {        http.csrf().disable();        http                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)                .and().authorizeRequests()                .antMatchers("/swagger-ui.html","/webjars/**").permitAll()                .antMatchers("/oauth/**").permitAll()                .antMatchers("/actuator/**").permitAll()                .antMatchers("/").permitAll()                .antMatchers(HttpMethod.OPTIONS).permitAll()                .anyRequest().permitAll();    }    @Override    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {        resources.authenticationEntryPoint(authenticationEntryPoint)                .accessDeniedHandler(accessDeniedHandler)                .resourceId("auth-server");    }}

3、SecurityConfig配置

package com.digquant.config;import com.digquant.service.CustomAuthenticationProvider;import com.digquant.service.OAuthUserService;import lombok.AllArgsConstructor;import org.springframework.boot.autoconfigure.AutoConfigureAfter;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.http.HttpMethod;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.AuthenticationProvider;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.builders.WebSecurity;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;@Order(7)@Configuration@EnableWebSecurity@AllArgsConstructor@AutoConfigureAfter(ResourceServerConfig.class)public class SecurityConfig extends WebSecurityConfigurerAdapter {    private final OAuthUserService userService;    @Override    protected void configure(HttpSecurity http) throws Exception {        http.cors().and().csrf().disable();        http.authorizeRequests()                .antMatchers("/oauth/**").permitAll()                .antMatchers("/public/**").permitAll()                .antMatchers("/actuator/**").permitAll()                .antMatchers("/private/**").permitAll()                .antMatchers("/").permitAll()                .antMatchers(HttpMethod.OPTIONS).permitAll()                .anyRequest().permitAll()                .and()                .sessionManagement()                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);    }    @Override    public void configure(WebSecurity web) throws Exception {        web.ignoring().antMatchers("/favor.ico");    }    @Bean    @Override    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {        auth.authenticationProvider(authenticationProvider());    }    @Bean    public PasswordEncoder passwordEncoder() {        return new BCryptPasswordEncoder();    }    @Bean    public AuthenticationProvider authenticationProvider() {        CustomAuthenticationProvider provider = new CustomAuthenticationProvider()                .setUserDetailsService(userService)                .setPasswordEncoder(passwordEncoder());        provider.setHideUserNotFoundExceptions(false);        return provider;    }}

4、JwkController 用于gateway 请求jwt私钥

package com.digquant.controller;import com.digquant.enity.JWTProperties;import com.nimbusds.jose.jwk.JWKSet;import com.nimbusds.jose.jwk.RSAKey;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.AllArgsConstructor;import org.springframework.security.oauth3.provider.token.store.KeyStoreKeyFactory;import org.springframework.web.bind.annotation.*;import java.security.interfaces.RSAPublicKey;import java.util.Map;@Api(tags = "jwk")@RestController@RequestMapping("/private")@AllArgsConstructorpublic class JwkController {    private final KeyStoreKeyFactory keyStoreKeyFactory;    private final JWTProperties jwtProperties;    @ApiOperation("获取jwk")    @PostMapping("/jwk_public_key")    public Map getKey() {        RSAPublicKey publicKey = (RSAPublicKey) keyStoreKeyFactory.getKeyPair(jwtProperties.getAuthJwtKey()).getPublic();        RSAKey key = new RSAKey.Builder(publicKey).build();        return new JWKSet(key).toJSONObject();    }}

注意私钥放到项目的resources目录下

5、用户鉴权服务,获取用户信息

package com.digquant.service;import com.digquant.enity.to.AuthenticationTO;import com.digquant.enums.LoginType;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;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.Component;import org.springframework.util.CollectionUtils;import java.util.List;import java.util.Map;@Slf4j@Componentpublic class OAuthUserService implements UserDetailsService {    @Autowired(required = false)    private List oAuthUserProcessors;    public UserDetails loadUser(String username, UsernamePasswordAuthenticationToken authentication) {        AuthenticationTO authenticationTO = new AuthenticationTO();        authenticationTO.setUsername(username);        authenticationTO.setPassword((String) authentication.getCredentials());        Map map = (Map) authentication.getDetails();        String scope = (String) map.get("scope");        String grantType = (String) map.get("grant_type");        String clientId = (String) map.get("client_id");        authenticationTO.setScope(scope);        authenticationTO.setGrantType(grantType);        authenticationTO.setLoginType(LoginType.PASSWORD);        authenticationTO.setClientId(clientId);        if (log.isDebugEnabled()) {            log.debug("请求认证参数:{}", authenticationTO);        }        if (!CollectionUtils.isEmpty(oAuthUserProcessors)) {            //目前只支持客户端密码登录方式            for (OAuthUserProcessor oAuthUserProcessor : oAuthUserProcessors) {                if (oAuthUserProcessor.support(authenticationTO)) {                    UserDetails userDetails = oAuthUserProcessor.findUser(authenticationTO);                    //TODO 需要加载OpenApi用户的权限                    loadAuthorities(userDetails, authenticationTO);                    return userDetails;                }            }        }        throw new UsernameNotFoundException("用户不存在");    }    private void loadAuthorities(UserDetails userDetails, AuthenticationTO authenticationTO) {    }    @Override    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {        return null;    }}

获取token

refresh_token

以上是"Spring gateway和Oauth2如何实现单点登录"这篇文章的所有内容,感谢各位的阅读!希望分享的内容对大家有帮助,更多相关知识,欢迎关注行业资讯频道!

0