具体流程

 * 网关鉴权流程:* 前端输入用户名密码去请求token,经过SecurityWeb配置,* 白名单不进入AuthorizationManager,直接进全局过滤器->没有token放行* 由网关转发/auth/** 到ouath2服务请求token** ouath2服务:* 请求token是白名单放行,通过SecurityUserDetails体系验证用户,发放token...* 前端拿到token,带token去请求用户信息** 网关拦截非白名单请求进入AuthorizationManager,无token直接拦截* 有token 通过框架spring-security-oauth2-resource-server去请求ouath2服务获取rsa公钥解析token* 再检验角色权限roleId,去redis或数据库拿到id列表,与token中的权限匹配* token解析成功 权限验证成功则进入全局过滤器,否则进入失败处理器* 进入全局过滤器验证一下token过期, 刷新, 修改密码(对比jti),然后再放行* 再经网关转发到具体服务** 第三方(qq)鉴权流程* 网关->qq登录请求是白名单,进入ouath2服务通过qqSDK请求qq登录的html* 前端弹出登陆qq页,扫码登陆qq,qq登录成功会回调我们ouath2服务的异步回调接口(要设白名单,通过域名访问是经过nginx)并带code码* 然后我们凭code码可以拿到qq头像,qq网名,然后我们也用qq头像做用户头像,qq的OpenId记录,经过自定义的token组装,生成token返回给前端* 这里:* 我们自己的登录体系是密码模式* qq第三方那边就是授权码模式了* 有优化的思路可以提出来一起分享,谢谢~

网关Gateway服务

  1. pom.xml
 springboot 2.3.3版本<!--注册中心客户端--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--网关--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--Spring-Gateway使用webflux--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><!--Gateway-Oauth2鉴权--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><!--redis + 连接池--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>
  1. 跨域配置文件
/*** Gateway网关全局跨域*/
@Configuration
public class Config_GatewayCors {@Beanpublic CorsWebFilter corsFilter() {CorsConfiguration config = new CorsConfiguration();config.setAllowCredentials(true);   // 允许cookies跨域config.addAllowedOrigin("*");       // 允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Originconfig.addAllowedHeader("*");       // 允许访问的头信息,*表示全部config.addAllowedMethod("*");       // 允许提交请求的方法类型,*表示全部允许config.setMaxAge(18000L);           // 预检请求的缓存时间-秒,即在这个时间段里,对于相同的跨域请求不会再预检了UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());source.registerCorsConfiguration("/**", config);return new CorsWebFilter(source);}
}
  1. web安全配置
/*** WebSecurity配置* 网关服务当做资源总服务*/
@Slf4j
@Configuration
@EnableWebFluxSecurity
public class Config_Security {@Resourceprivate AuthorizationManager authorizationManager;@Beanpublic SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {log.info("1");http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());//自定义Token过期或签名错误的结果http.oauth2ResourceServer().authenticationEntryPoint(new SecurityAuthenticationEntryPoint());//对白名单路径直接移除Tokenhttp.addFilterBefore(new IgnoreUrlsRemoveJwtFilter(), SecurityWebFiltersOrder.AUTHENTICATION);//配置白名单和访问规则,CommonEnum枚举类http.csrf().disable().authorizeExchange().pathMatchers(CommonEnum.urls.toArray(new String[]{})).permitAll().anyExchange().access(authorizationManager).and().exceptionHandling().accessDeniedHandler(new SecurityAccessDeniedHandler());return http.build();}/*** ServerHttpSecurity没有将jwt中authorities的负载部分当做Authentication,需要把jwt的Claim中的authorities加入* 定义ReactiveAuthenticationManager权限管理器,默认转换器JwtGrantedAuthoritiesConverter*/@Beanpublic Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();jwtGrantedAuthoritiesConverter.setAuthorityPrefix(CommonEnum.AUTHORITY_PREFIX);jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(CommonEnum.AUTHORITY_CLAIM_NAME);JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);}/*** token认证失败处理器*/static class SecurityAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {@Overridepublic Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.OK);response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);CommonResponse<?> responseCon = new CommonResponse<>(401,"身份认证失败", e.getMessage());String body= JSON.toJSONString(responseCon);DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(buffer));}}/*** 权限不足处理器*/static class SecurityAccessDeniedHandler 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);CommonResponse<?> responseCon = new CommonResponse<>(401, "没有权限", denied.getMessage());String body= JSON.toJSONString(responseCon);DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(buffer));}}/*** 白名单路径访问时需要移除token请求头*/static class IgnoreUrlsRemoveJwtFilter implements WebFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {ServerHttpRequest request = exchange.getRequest();URI uri = request.getURI();PathMatcher pathMatcher = new AntPathMatcher();for (String ignoreUrl: CommonEnum.urls) {if (pathMatcher.match(ignoreUrl, uri.getPath())) {request = exchange.getRequest().mutate().header("Authorization", "").build();exchange = exchange.mutate().request(request).build();return chain.filter(exchange);}}return chain.filter(exchange);}}
}
  1. redis配置

/*** redis配置*/
@Configuration
public class Config_Redis {/*** 序列化防止存到redis中乱码, ConditionalOnMissingBean必须定义这个名字,不然springboot会有默认的*/@Bean@ConditionalOnMissingBean(name = "redisTemplate")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(connectionFactory);StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(stringRedisSerializer);redisTemplate.setHashKeySerializer(stringRedisSerializer);Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);redisTemplate.afterPropertiesSet();return redisTemplate;}
}
  1. 一些字段
/*** 通用枚举类*/
public class CommonEnum {//放行白名单public final static List<String> urls =Arrays.asList("/cloud-oauth2/QQLogin",        //请求QQ登录放行"/cloud-oauth2/qq_notify_url",  //请求QQ登录异步回调放行"/GetVerifyCode",       //请求验证码放行"/rsa/publicKey",       //请求公钥放行"/actuator/**",         //请求oauth2放行"/auth/oauth/token",    //请求oauth2放行"/shop-member/Register",  //请求用户注册放行"/**/GetHomeProduct",     //请求商城首页数据放行"/**/SearchGoods",        //请求搜索商品放行"/**/ali_return_url",      //支付宝同步回调放行"/**/ali_notify_url"       //支付宝异步回调放行);//redis权限列表keypublic final static String RESOURCE_ROLES_MAP = "AUTH:RESOURCE_ROLES_MAP";//权限的前置部分public final static String AUTHORITY_PREFIX = "ROLE_";public final static String AUTHORITY_CLAIM_NAME = "authorities";//token标识public final static String TOKEN_HEADER = "Authorization";//发往其他服务的请求头标识public final static String USER_HEADER = "USER_HEADER";
}
  1. 全局过滤器
/*** 全局过滤器*/
@Slf4j
@Component
public class AuthGlobalFilter implements GlobalFilter {@Resourceprivate RedisTemplate<String, Object> redisTemplate;//白名单的请求会经过过滤器@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("全局过滤器:{}",exchange.getRequest().getPath());String token = exchange.getRequest().getHeaders().getFirst(CommonEnum.TOKEN_HEADER);if (token == null || token.equals("")) {return chain.filter(exchange); //白名单如果没有token, 直接放行}try {//从Token中解析用户信息并设置到Header中去JWSObject jwsObject = JWSObject.parse(token.replace("Bearer ", ""));String userStr = jwsObject.getPayload().toString();//token(过期, 刷新, 修改密码)校验Map<String, Object> maps = (Map) JSON.parse(userStr);//请求token中的jtiString oldJti = (String) maps.get("jti");//创建新token时存到redis的jtiString newJti = (String) redisTemplate.opsForValue().get(maps.get("id") + (String) maps.get("user_name"));//校验jti字段判断token是否失效if (!oldJti.equals(newJti)) {ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.OK);response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);CommonResponse<?> responseCon = new CommonResponse<>(401, "error", "Token令牌失效");String body = JSON.toJSONString(responseCon);DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(buffer));}//没有失效,向headers中放数据,后续服务就不需要解析JWT令牌,可以直接从请求的Header中获取到用户信息ServerHttpRequest request = exchange.getRequest().mutate().header(CommonEnum.USER_HEADER, userStr).build();exchange = exchange.mutate().request(request).build();} catch (Exception e) {log.error("过滤器发生错误:{}", e.getMessage());}//如果没有tokenreturn chain.filter(exchange);}}
  1. 鉴权管理器
/*** 鉴权管理器*/
@Slf4j
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {@Resourceprivate RedisTemplate<String,Object> redisTemplate;/*** 不是白名单请求经过这里,先校验可访问角色列表*/@Overridepublic Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {log.info("2");ServerHttpRequest request = authorizationContext.getExchange().getRequest();String path = request.getURI().getPath();if (request.getMethod() == HttpMethod.OPTIONS) {log.info("对跨域的预检请求直接放行,路径为:{}",path);return Mono.just(new AuthorizationDecision(true));}String token = request.getHeaders().getFirst(CommonEnum.TOKEN_HEADER);log.info(path+"路径,token:{}",token);if(token==null || token.equals("")){//token为空不放行return Mono.just(new AuthorizationDecision(false));}//从redis中取出权限列表Object obj = redisTemplate.opsForHash().get(CommonEnum.RESOURCE_ROLES_MAP, path);List<String> authorities = new ArrayList<>();if (obj instanceof ArrayList<?>) {for (Object o : (List<?>) obj) {authorities.add(CommonEnum.AUTHORITY_PREFIX + o);}}return mono.filter(Authentication::isAuthenticated).flatMapIterable(Authentication::getAuthorities).map(GrantedAuthority::getAuthority).any(roleId -> true) //我这边不做鉴权 就返回true了,需要鉴权就写 authorities.contains(roleId).map(AuthorizationDecision::new).defaultIfEmpty(new AuthorizationDecision(false));}
}
  1. 网关使用bootstarp.yml
server:port: 8080spring:main:allow-bean-definition-overriding: true  #当遇到同样名字的时候,是否允许覆盖注册application:name: cloud-gatewaycloud:nacos:discovery:server-addr: ip:8848config:server-addr: ip:8848   #NaCos配置中心地址file-extension: yamlusername: nacospassword: nacosgateway:discovery:locator:enabled: true                #是否与服务发现组件进行结合,通过serviceId转发到具体的服务实例lower-case-service-id: true  #使用小写service-idroutes:- id: route1uri: lb://cloud-oauth2predicates:- Path=/auth/**filters:- StripPrefix=1    #截取掉auth,当请求是白名单login,获取token,会经过这里转发到验证服务security:oauth2:resourceserver:jwt:jwk-set-uri: http://cloud-oauth2:9988/rsa/publicKey  #获取公钥路径,docker-compose下才能使用cloud-oauth2域名,否则使用ip(需要查看验证服务器的docker容器ip)#redisredis:host: redisport: 6379lettuce:pool:max-idle: 100     #最大空闲连接数min-idle: 20      #最小空闲连接数max-wait:  -1s    #等待可用连接的最大时间,负数为不限制max-active: -1    #最大活跃连接数,负数为不限制password: xxxxxx#指定日志文件
logging:config: classpath:logback-logstash.xml

oauth2验证服务,集成QQ登录

  1. pom.xml
        <!--oauth2 + JWT工具--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>com.nimbusds</groupId><artifactId>nimbus-jose-jwt</artifactId><version>9.1.3</version></dependency><!--QQ登录依赖--><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.9</version></dependency><dependency><groupId>net.gplatform</groupId><artifactId>Sdk4J</artifactId><version>2.0</version></dependency>...redis和mysql略
  1. application.yml
server:port: 9988spring:profiles:include: com-nacos,com-redis,com-sql,com-openfeignapplication:name: cloud-oauth2myOauth2:clientId: admin1clientSecret: 123456qq:oauth:appid: xxxappkey: xxxhttp: http://www.lyhosiris.cn/callback_url: http://www.lyhosiris.cn/api/cloud-oauth2/qq_notify_url   #QQ互联中填写的回调地址
  1. QQ请求工具
public class QQHttpClient {private static JSONObject parseJSONP(String jsonp){int startIndex = jsonp.indexOf("(");int endIndex = jsonp.lastIndexOf(")");String json = jsonp.substring(startIndex + 1,endIndex);return JSONObject.parseObject(json);}//qq返回token信息 access_token=xxx&expires_in=xxx&refresh_token=xxxpublic static String getAccessToken(String url) throws IOException {CloseableHttpClient client = HttpClients.createDefault();String token = null;HttpGet httpGet = new HttpGet(url);HttpResponse response = client.execute(httpGet);HttpEntity entity = response.getEntity();if(entity != null){String result = EntityUtils.toString(entity,"UTF-8");if(result.contains("access_token")){String[] array = result.split("&");for (String str : array){if(str.contains("access_token")){token = str.substring(str.indexOf("=") + 1);break;}}}}httpGet.releaseConnection();return token;}//qq返回openid信息 callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} ); 需要用到上面自己定义的解析方法parseJSONPpublic static String getOpenID(String url) throws IOException {JSONObject jsonObject = null;CloseableHttpClient client = HttpClients.createDefault();HttpGet httpGet = new HttpGet(url);HttpResponse response = client.execute(httpGet);HttpEntity entity = response.getEntity();if(entity != null){String result = EntityUtils.toString(entity,"UTF-8");jsonObject = parseJSONP(result);}httpGet.releaseConnection();if(jsonObject != null){return jsonObject.getString("openid");}else {return null;}}//qq返回用户信息 { "ret":0, "msg":"", "nickname":"YOUR_NICK_NAME", ... },为JSON格式,直接使用JSONObject对象解析public static JSONObject getUserInfo(String url) throws IOException {JSONObject jsonObject = null;CloseableHttpClient client = HttpClients.createDefault();HttpGet httpGet = new HttpGet(url);HttpResponse response = client.execute(httpGet);HttpEntity entity = response.getEntity();if(entity != null){String result = EntityUtils.toString(entity,"UTF-8");jsonObject = JSONObject.parseObject(result);}httpGet.releaseConnection();return jsonObject;}
}
  1. 通用枚举类
public class CommonEnum {//白名单public final static List<String> urls =Arrays.asList("/QQLogin",        //请求QQ登录放行"/qq_notify_url",  //请求QQ登录异步回调放行"/rsa/publicKey"   //请求公钥放行);public final static String RESOURCE_ROLES_MAP = "AUTH:RESOURCE_ROLES_MAP";public final static String AUTHORITY_PREFIX = "ROLE_";public final static String AUTHORITY_CLAIM_NAME = "authorities";public final static String TOKEN_HEADER = "Authorization";public final static String USER_HEADER = "USER_HEADER";
}
  1. web安全配置类

/*** Security配置类* EnableWebSecurity开启Security*/
@Slf4j
@Configuration
@EnableWebSecurity
public class Config_WebSecurity extends WebSecurityConfigurerAdapter {/*** 用来配置拦截保护的请求,这里配置所有请求都需要认证*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers(CommonEnum.urls.toArray(new String[]{})).permitAll() //白名单放行.anyRequest().authenticated() //所有请求都需要通过认证.and().httpBasic()            //Basic提交.and().csrf().disable().formLogin().permitAll();     //支持表单认证}/*** 配置验证管理器*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}
  1. 配置验证服务类
/*** 验证服务器, EnableAuthorizationServer注解表示是个验证服务器*/
@Slf4j
@Configuration
@EnableAuthorizationServer
public class Config_Authorization extends AuthorizationServerConfigurerAdapter {@Value("${myOauth2.clientId}")private String clientId;@Value("${myOauth2.clientSecret}")private String clientSecret;@Resourceprivate AuthenticationManager authenticationManager; //注入安全管理器@Resourceprivate MyPasswordEncoder myPasswordEncoder;         //注入我们的密码工具类@Resourceprivate UserServiceImpl userDetailsService;          //注入用户包装实现类//    //============redis存储token===============
//    @Resource
//    private RedisConnectionFactory redisConnectionFactory;//=============JWT存储token==================@Resourceprivate TokenStore tokenStore;@Resourceprivate JwtAccessTokenConverter accessTokenConverter;@Resourceprivate Jwt_TokenEnhancer jwtTokenEnhancer;/*** 访问端点配置,配置授权authorization以及令牌(token)的访问端点和令牌服务(token services)*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//配置Redis存储token//endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory));//配置jwt存储token + jwt自定义内容增强TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, accessTokenConverter));endpoints.tokenStore(tokenStore).accessTokenConverter(accessTokenConverter).tokenEnhancer(tokenEnhancerChain);//配置管理器允许GET和POST请求获取token;即访问端点oauth/tokenendpoints.authenticationManager(authenticationManager).allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);//要使用refresh_token的话,需要额外配置userDetailsServiceendpoints.userDetailsService(userDetailsService);}/*** 授权端点开放,配置令牌端点(Token Endpoint)的安全约束,也就是这个端点谁能访问,谁不能访问*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {oauthServer.tokenKeyAccess("permitAll()")              //开启/oauth/token_key验证端口无权限访问.checkTokenAccess("isAuthenticated()")      //开启/oauth/check_token验证端口认证权限访问.allowFormAuthenticationForClients();       //允许表单认证}/*** 配置客户端详情服务,客户端详情信息在这里进行初始化,通过数据库来存储调取详情信息*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {//使用内存模式,也可以配置客户端存储到数据库DB,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息clients.inMemory().withClient(clientId)                                                       //client_id.secret(myPasswordEncoder.encode(clientSecret))                             //client_密码.accessTokenValiditySeconds(3600)                                           //配置刷新token的有效期.refreshTokenValiditySeconds(864000)                                        //配置刷新token的有效期.scopes("all").authorizedGrantTypes("password", "refresh_token");                         //授权类型:密码模式}
}
  1. jwt 存储配置类
/*** 使用Jwt存储token的配置*/
@Configuration
public class Jwt_TokenStore {@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(accessTokenConverter());}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setKeyPair(keyPair());return converter;}@Beanpublic Jwt_TokenEnhancer jwtTokenEnhancer() {return new Jwt_TokenEnhancer();}/*** 非对称加密rsa 需要证书文件* resource中的jwt.jks文件是使用keytool生成RSA证书jwt.jks* jdk安装目录bin中,cmd* 命令:keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks* 输入你的密码,城市等等信息生成证书jwt.jks文件* * 生成成功后在jdk安装目录bin中* 复制到resource目录下*/@Beanpublic KeyPair keyPair() {//从classpath下的证书中获取秘钥对,123456是密码KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());}
}

  1. 配置jwt内容增强器

/*** Jwt内容增强器*/
@Slf4j
public class Jwt_TokenEnhancer implements TokenEnhancer {@Resourceprivate RedisTemplate<String,Object> redisTemplate;@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {SecurityUserDetails securityUser = (SecurityUserDetails) authentication.getPrincipal();log.info("用户信息{}",securityUser.toString());//log.info("权限列表{}",authentication.getAuthorities());Map<String, Object> info = new HashMap<>();//把用户信息设置到jwt载荷与redis中info.put("id", securityUser.getId());try{redisTemplate.opsForValue().set(securityUser.getId()+securityUser.getUsername(),accessToken.getValue());}catch (Exception e){e.printStackTrace();}((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);return accessToken;}
}

还有就是redis的配置文件,用网关中的

  1. Security用户管理业务类
/*** Security用户管理业务类*/
@Service
public class UserServiceImpl implements UserDetailsService {@Resourceprivate IUmsMemberService umsMemberService;//根据用户名查数据库@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {UmsMember umsMember = umsMemberService.GetByNameMember(username);if(umsMember==null) {throw new UsernameNotFoundException("用户名或密码错误");}SecurityUserDetails details = new SecurityUserDetails(umsMember,Arrays.asList("USER"));if (!details.isEnabled()) {throw new DisabledException("用户未启用");} else if (!details.isAccountNonLocked()) {throw new LockedException("用户被锁定");} else if (!details.isAccountNonExpired()) {throw new AccountExpiredException("用户已过期");} else if (!details.isCredentialsNonExpired()) {throw new CredentialsExpiredException("密码已过期");}return details;}
}
  1. 生成token服务类
interface MyTokenService {String createMyToken(Long id, String openId, Map<String, String> parameters, MyPasswordEncoder myPasswordEncoder) ;
}@Slf4j
@Service
public class MyTokenServiceImpl implements MyTokenService {@Value("${myOauth2.clientId}")private String clientId;@Value("${myOauth2.clientSecret}")private String clientSecret;@Resourceprivate TokenStore tokenStore;@Resourceprivate JwtAccessTokenConverter accessTokenConverter;@Resourceprivate Jwt_TokenEnhancer jwtTokenEnhancer;@Resourceprivate ClientDetailsService clientDetailsService;@Overridepublic String createMyToken(Long id, String openId, Map<String, String> parameters, MyPasswordEncoder passwordEncoder) {parameters.put("client_id", clientId);parameters.put("client_secret", clientSecret);parameters.put("grant_type", "password");DefaultTokenServices defaultTokenServices = new DefaultTokenServices();defaultTokenServices.setSupportRefreshToken(true);defaultTokenServices.setTokenStore(tokenStore);TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, accessTokenConverter));defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);TokenRequest tokenRequest = new TokenRequest(parameters, clientId, Collections.singleton("all"),"password");OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);//构造权限列表GrantedAuthority grantedAuthority = new GrantedAuthority() {@Overridepublic String getAuthority() {return "USER";}};//初始密码abc123SecurityUserDetails userDetails = new SecurityUserDetails(id, openId, passwordEncoder.encode("abc123"),Arrays.asList("USER"));//创建UsernamePasswordAuthenticationTokenAuthentication userAuth = new UsernamePasswordAuthenticationToken(userDetails, "[PROTECTED]", Arrays.asList(grantedAuthority));//将OAuth2Request 和 Authorization 两个对象组合起来形成一个 OAuth2Authorization 对象OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, userAuth);//OAuth2Authentication对象会传递到AuthorizationServerTokenServices的实现类DefaultTokenServices中;最终会生成一个OAuth2AccessTokenOAuth2AccessToken accessToken = defaultTokenServices.createAccessToken(oAuth2Authentication);//返回我们的生成的tokenreturn accessToken.getValue();}
}
  1. 自定义密码加密类
@Component
public class MyPasswordEncoder implements PasswordEncoder {private final String salt = "yan";@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return encodedPassword.equals(encode(rawPassword));}@Overridepublic String encode(CharSequence rawPassword) {String base = rawPassword + salt;return DigestUtils.md5DigestAsHex(base.getBytes());}
}
  1. 登录的controller
@Slf4j
@RestController
public class QQLoginController {@Value("${qq.oauth.callback_url}")private String callback_url;@Value("${qq.oauth.appid}")private String APPID;@Value("${qq.oauth.appkey}")private String APPKEY;@Resourceprivate MyTokenService myTokenService;@Resourceprivate IUmsMemberService umsMemberService;@Resourceprivate MyPasswordEncoder myPasswordEncoder;@Resourceprivate KeyPair keyPair;//获取RSA公钥接口@RequestMapping("/rsa/publicKey")public Map<String, Object> getKey() {RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();RSAKey key = new RSAKey.Builder(publicKey).build();return new JWKSet(key).toJSONObject();}/*** 发起QQ登录请求*/@RequestMapping("/QQLogin")public CommonResponse<String> QQLogin(HttpSession session) {//用于第三方应用防止CSRF攻击String uuid = UUID.randomUUID().toString().replaceAll("-", "");session.setAttribute("state", uuid);//1: 获取Authorization CodeString url = "https://graph.qq.com/oauth2.0/authorize?response_type=code" +"&client_id=" + APPID +"&redirect_uri=" + URLEncoder.encode(callback_url) +"&state=" + uuid;return new CommonResponse<>(200, "success", url);}/*** QQ发起登录后的异步回调方法;直接通过域名访问是经过nginx所以地址要根据nginx的转发规则* http://www.lyhosiris.cn/api/cloud-oauth2/qq_notify_url*/@RequestMapping("/qq_notify_url")public String  qq_notify_url(HttpServletRequest request, HttpServletResponse response) throws Exception {response.setContentType("text/html; charset=utf-8");HttpSession session = request.getSession();//QQ返回的信息:http://graph.qq.com/demo/index.jsp?code=xxx&state=xxxString code = request.getParameter("code");String state = request.getParameter("state");String uuid = (String) session.getAttribute("state");if (uuid != null) {if (!uuid.equals(state)) {throw new Exception("QQd的state错误");}}//2: 通过Authorization Code获取Access TokenString url = "https://graph.qq.com/oauth2.0/token?grant_type=authorization_code" +"&client_id=" + APPID + "&client_secret=" + APPKEY + "&code=" + code + "&redirect_uri=" + callback_url;String access_token = QQHttpClient.getAccessToken(url);//3: 获取回调后的 openid 值url = "https://graph.qq.com/oauth2.0/me?access_token=" + access_token;String openId = QQHttpClient.getOpenID(url);//4: 获取QQ用户信息url = "https://graph.qq.com/user/get_user_info?access_token=" + access_token +"&oauth_consumer_key=" + APPID + "&openid=" + openId;//可放到Redis和mysql中JSONObject jsonObject = QQHttpClient.getUserInfo(url);String nkName = (String) jsonObject.get("nickname");         //QQ网名String iconUrl = (String) jsonObject.get("figureurl_qq_2");  //100*100像素的QQ头像urlMap<String, String> parameters = new HashMap<>();UmsMember ub = umsMemberService.CheckMemberOpenIdExists(openId);             //根据openID查数据库有没有这个用户Long id = null;if (ub != null && ub.getOpenid() != null) {  //如果有这个用户id = ub.getId();parameters.put("username", ub.getUsername());parameters.put("password", ub.getPassword());} else {                                                                     //如果没有就去注册账号String password = myPasswordEncoder.encode("abc123");        //自定义初始密码assert openId != null;String newUsername = openId.substring(0,10);//远程调用用户服务,注册账号Long rpc_user_id = umsMemberService.QQRegister(openId, newUsername, nkName, password, iconUrl);if (rpc_user_id != null) {                                               //注册成功返回idid = rpc_user_id;parameters.put("username", newUsername);parameters.put("password", password);} else {return "window.alert('系统错误,注册失败');<script>window.close();</script>";  //注册失败...直接关闭}}//注册或or登录成功返回我们自定义的tokenString token = myTokenService.createMyToken(id, openId, parameters, myPasswordEncoder);return "<script>window.opener.localStorage.setItem('Authorization', '\""+token+"\"');window.close();</script>";}
  1. Security包装的用户信息,重要

/*** Security包装的用户信息*/
@Data
public class SecurityUserDetails implements UserDetails {private Long id;private String username;private String password;//用户状态private Boolean enabled;//权限数据private Collection<SimpleGrantedAuthority> authorities;//返回当前用户的权限@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}public SecurityUserDetails(UmsMember umsMember, List<String> roles) {this.id = umsMember.getId();this.username = umsMember.getUsername();this.password = umsMember.getPassword();this.enabled = umsMember.getStatus() == 1;if (roles != null && roles.size() > 0) {authorities = new ArrayList<>();roles.forEach(item -> authorities.add(new SimpleGrantedAuthority(item)));}}public SecurityUserDetails(long id ,String username, String password, List<String> roles) {this.id = id;this.username = username;this.password = password;this.enabled = true;if (roles != null && roles.size() > 0) {authorities = new ArrayList<>();roles.forEach(item -> authorities.add(new SimpleGrantedAuthority(item)));}}/*** 账户是否不过期,false即过期*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 是否不上锁,false即上锁*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 密码是否不过期,false即过期*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否启用*/@Overridepublic boolean isEnabled() {return true;}
}

Vue

登录页的 methods 方法中//QQ登录方法 axios  ToQQLogin(){var _this = this;QQLogin().then(response=>{if(response.data.code == 200){var width = width || 720; var height = height || 460;var left = (window.screen.width - width) / 2; var top = (window.screen.height - height) / 2;var win = window.open(response.data.data,"_blank","toolbar=yes,location=yes,directories=no,status=no,menubar=yes,scrollbars=yes,resizable=no,copyhistory=yes,left="+left+",top="+top+",width="+width+",height="+height);//监听登录窗口是否关闭,登录成功后端返回关闭窗口的代码;setInterval()方法会不停地调用函数,直到clearInterval()被调用或窗口被关闭var listener = setInterval(function() {//如果关闭了if(win.closed){if(getToken()){ //判断token是否存在_this.$store.commit('SET_TOKEN', getToken());clearInterval(listener);    _this.$router.push('/layout');  //跳转到首页,首页里根据token请求用户信息资料...} else {clearInterval(listener); //关闭弹框}}},500);}});}

请求的axios

个人写的网站:www.lyhosiris.cn

点击qq图标,请求验证服务器的接口 /QQLogin,返回一个url连接
这个连接用作用是:请求qq服务器的授权码Code【读过我前面文章ouath2授权码模式的应该知道】
用这个url打开一个 (windows.open) 新窗口去请求QQ服务器获取授权码

qq登录成功则返回 【授权码code】 到我们的回调地址,就是qq互联的回调地址
也就是这里
这里在调用qq请求工具,直接在服务端再次请求qq服务器获取token,openid,个人信息等等

然后解析,查看数据库中用户表中有没有这个openid的用户

如果注册失败则返回关闭弹框的js代码,前端就登录失败…

如果登录成功或注册成功则生成自定义token返回给前端存到 localStorage 中

生成token的代码是核心
我们的网站有两个登录的
1.我们自己网站的登录方法
2.第三方qq登录
3.微信登录…(还没做TT)

第一个:我们网站自己的登录,用的是security - oauth2中的密码模式

经过网关,请求token,验证服务器返回token,vue把token放在请求头中,请求用户数据

返回的token是经过验证服务器的/oauth/token端点返回的
我们这里配置过怎么生成的,以及自定义增强啊,和存储到哪。在这个类中

/oauth/token端点的执行的方法是框架内部的

我在代码中使用
@Resource
TokenEndpoint tokenEndpint;
注入,调用getAccessToken,不能执行成功
所以我们只能手动生成,分析他的工作流程

有一个实现类可以帮我们做:DefaultTokenServices

把我们的token增强类和存储类放入里面
然后构造一些【密码模式】必要的类和属性,这里初始密码当然要和前面注册的初始密码一样的才行,这个密码用户也不用知道。

然后创建权限列表(当然你也可以用数据库查询处理)
再创建包装的SecurityUserDetails,使得支持同样的校验

Authentication userAuth = new UsernamePasswordAuthenticationToken(userDetails, "[PROTECTED]", Arrays.asList(grantedAuthority));
这个就是密码模式的生成token的工具了,我们把SecurityUserDetails,和创建的权限列表放入生成Authentication

然后使用defaultTokenServices.createAccessToken() 生成token对象。当然也可以生成刷新token等等

这时候前端不管是qq登录,还是我们自己网站登录,使用rsa公钥,调用验证服务器校验端点,去校验token都是没问题的

这就是第三方和网关的统一登录,欢迎指出缺点

SpringCloud-Gateway网关统一登录鉴权+QQ第三方登录+Vue前后分离解决方案相关推荐

  1. java实现支付宝第三方登录_Java 实现QQ第三方登录(附赠:完整代码)

    老铁,转发+关注+私信 获取完整代码 前言:很多时候我们都需要如下的第三方登录,用QQ帐号快速登录你的网站,降低注册门槛,为你的网站带来海量新用户. 下面让我们来实现吧,可以参考官网文档,也可以看我下 ...

  2. php如何实现qq第三方登录,PHP实现qq第三方登录

    除了qq第三方登录外.还有微博,微信等第三方登录 qq第三方登录,遵循oauth2.0协议 这里是说明http://www.cnblogs.com/yx520zhao/p/6616686.html q ...

  3. android qq三方登录授权失败,QQ第三方登录无法授权错误码110401的解决方法

    原标题:QQ第三方登录无法授权错误码110401的解决方法 一些网友在注册APP的时候,会选择QQ作为第三方登录方式,但是,最近,一些网友发现:选择QQ第三方登录的时候,会出现无法授权错误码11040 ...

  4. java qq登录_JAVA实现QQ第三方登录

    首先在QQ互联: https://connect.qq.com/manage.html 申请账号,并且进行资料审核,同时创建应用(设置回调地址) 申请应用完后,会有app_ID.app_KEY等参数 ...

  5. SpringCloud Gateway网关统一聚合Swagger接口文档(knife4j),实现通过网关统一文档地址查看所有子服务的接口文档

    前言: 在微服务系统中,通常每个服务都会暴露其接口文档,在前端人员或测试人员查看的时候,并不是那么方便,我们需要告诉相关人员每个服务的文档地址,由于swagger/knif4j(knif4j为更易用的 ...

  6. qq第三方登录注册php,QQ第三方登录PHP

    1.授权登录 http://openapi.qzone.qq.com/oauth/show?which=ConfirmPage&display=pc&response_type=cod ...

  7. OAuth模块管理客户端的用户登录鉴权功能,允许应用访问第三方平台的资源

    OAuth接口支持开发者调用当前环境中安装的三方客户端App(如微信.微博等)的授权登录页面进行鉴权操作. 若终端安装了对应的客户端App,则调用客户端的授权登录页面,否则调用WAP页面进行授权登录. ...

  8. 企鹅电竞登录鉴权系统架构与核心数据热备容灾方案

    文章目录 0.前言 1.术语说明 2.登录鉴权系统架构 3.登录鉴权关键路径梳理与优化 4.核心数据热备容灾 4.1 需求背景 4.2 其它系统容灾方案 4.2.1 NOW 直播评论容灾架构 4.2. ...

  9. 公司网站如何让用户使用QQ第三方登录

    我们做了一个中农物联网系统,为了简化用户使用门槛,并保证用户资料安全,准备在自身的用户管理基础上加入第三方登录,主要是QQ登录.微信二维码扫描登录.手机微信登录等.     QQ第三方登录,用的是QQ ...

最新文章

  1. 同事把实数作为 HashMap 的key,领导发飙了...
  2. 【Netty】Netty为什么要手动释放ByteBuf资源?
  3. LeetCode 3 无重复字符的最长子串
  4. 【Webview相关问题】登陆失败之cookie陷阱
  5. 【c++】27.事件驱动、IO复用、sellect、poll、epoll三者的区别
  6. 实事求是来讲,比较艰难的环境能够激发人的斗志
  7. ios 获取一个枚举的所有值_凯哥带你从零学大数据系列之Java篇---第十一章:枚举...
  8. Angular 服务器端渲染的学习笔记(二)
  9. CString类(转)
  10. [Unity][FlowCanvas] 被重复执行的节点,只要其中一次执行出现报错,该报错就会在脚本中一直显示
  11. Django中使用缓存
  12. 什么叫做石英表_什么是石英表 石英表是什么意思
  13. 无法远程xp服务器,五步快速处理在WinXP下IIS无法远程访问的问题
  14. 回头再说-006 时间音乐
  15. Centos7 Kubernetes(k8s) 开发服务器(单服务器)部署 路由 IngressRoute【traefik2.X】
  16. 5分钟转换PDF为图片
  17. Endnote常见错误
  18. 为防泄密 新加坡政府将断掉公务员的网络连接
  19. CodeForces 950C Zebras
  20. 80后最牛的辞职信+出师表

热门文章

  1. 混合IT的新世界 存储工程师的江湖地位不保?
  2. PC端无线连接打印机
  3. avue设置表格显示图片
  4. 以圆桌骑士为例浅尝HTML5游戏开发
  5. 【739】单调栈应用
  6. Java毕设项目电力公司员工安全培训系统(java+VUE+Mybatis+Maven+Mysql)
  7. 这样做框架结构图,让你的PPT更有创意!
  8. MOOC TensorFlow入门实操课程代码回顾总结(三)
  9. rman 备份脚本之总结分析
  10. 国大开放英语计算机考试答案,国家开放大学电大专科《开放英语1》期末试题标准题库及答案.docx...