搭建微服务还是异常的艰难呀.....
本来想就在web_portal下整合springsecurity 但是又想既然搭建了gateway(网关),又为何不直接集成到网关当中呢
说干就干
##引入maven
<!-->spring-boot 整合security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!-- redis依赖需要 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <!--openfeign--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
创建SecurityConfig主配置文件
package com.mysb.core.config; import com.mysb.core.server.AuthenticationFaillHandler; import com.mysb.core.server.AuthenticationSuccessHandler; import com.mysb.core.server.CustomHttpBasicServerAuthenticationEntryPoint; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.server.SecurityWebFilterChain; @Configuration @EnableWebFluxSecurity public class SecurityConfig{ @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFaillHandler authenticationFaillHandler; @Autowired private CustomHttpBasicServerAuthenticationEntryPoint customHttpBasicServerAuthenticationEntryPoint; //security的鉴权排除列表 private static final String[] excludedAuthPages = { "/login", "/logout", "/home/**", "/user/**", "/category/**" }; @Bean SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception { http .cors() .and() .authorizeExchange() .pathMatchers(excludedAuthPages).permitAll() //无需进行权限过滤的请求路径 .pathMatchers(HttpMethod.OPTIONS).permitAll() //option 请求默认放行 .anyExchange().authenticated() .and() .httpBasic() .and() .formLogin() .authenticationSuccessHandler(authenticationSuccessHandler) //认证成功 .authenticationFailureHandler(authenticationFaillHandler) //登陆验证失败 .and().exceptionHandling().authenticationEntryPoint(customHttpBasicServerAuthenticationEntryPoint) //基于http的接口请求鉴权失败 .and() .csrf().disable()//必须支持跨域 .logout().disable(); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); //默认 } } }
这个写法是springwebFlux而不是springMVC,因为gateway底层是用netty,基于webFlux的,跟SpringMVC传统方式是不兼容的,详细看下这位大神https://blog.csdn.net/tiancao222/article/details/104375924
配置spring security还是跟以前一样
创建成功拦截器
因为前后端分离axios异步不能有重定向 就只能用拦截器来返回给前端参数
package com.mysb.core.server; import com.alibaba.csp.ahas.shaded.com.alibaba.acm.shaded.com.google.gson.JsonObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.mysb.core.utils.MessageCode; import com.mysb.core.utils.WsResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.util.UUID; import java.util.concurrent.TimeUnit; @Component public class AuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler { @Autowired private RedisTemplate redisTemplate; @Override public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication){ System.out.println("success"); ServerWebExchange exchange = webFilterExchange.getExchange(); ServerHttpResponse response = exchange.getResponse(); //设置headers HttpHeaders httpHeaders = response.getHeaders(); httpHeaders.add("Content-Type", "application/json; charset=UTF-8"); httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); httpHeaders.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Authorization"); //设置body WsResponse wsResponse = WsResponse.success(); byte[] dataBytes={}; ObjectMapper mapper = new ObjectMapper(); try { String uuid = UUID.randomUUID().toString().replaceAll("-", ""); httpHeaders.add(HttpHeaders.AUTHORIZATION, uuid); wsResponse.setResult(authentication.getName()); //保存token redisTemplate.boundValueOps(uuid).set(authentication.getName(), 2*60*60, TimeUnit.SECONDS); dataBytes=mapper.writeValueAsBytes(wsResponse); } catch (Exception ex){ ex.printStackTrace(); JsonObject result = new JsonObject(); result.addProperty("status", MessageCode.COMMON_FAILURE.getCode()); result.addProperty("message", "授权异常"); dataBytes=result.toString().getBytes(); } DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes); System.out.println(wsResponse); return response.writeWith(Mono.just(bodyDataBuffer)); } }
这里的写法也是webfulx的,这里不展开讨论 wsResponse则是自定义返回前端的参数 参考的是这篇文章https://blog.csdn.net/MongolianWolf/article/details/94329980
失败拦截器
@Component public class AuthenticationFaillHandler implements ServerAuthenticationFailureHandler { @Override public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) { System.out.println("fail"); ServerWebExchange exchange = webFilterExchange.getExchange(); ServerHttpResponse response = exchange.getResponse(); //设置headers HttpHeaders httpHeaders = response.getHeaders(); httpHeaders.add("Content-Type", "application/json; charset=UTF-8"); httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); //设置body WsResponse<String> wsResponse = WsResponse.failure(MessageCode.COMMON_AUTHORIZED_FAILURE); byte[] dataBytes={}; try { ObjectMapper mapper = new ObjectMapper(); dataBytes=mapper.writeValueAsBytes(wsResponse); } catch (Exception ex){ ex.printStackTrace(); } DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes); return response.writeWith(Mono.just(bodyDataBuffer)); } }
http的接口请求鉴权失败
package com.mysb.core.server; import com.alibaba.csp.ahas.shaded.com.alibaba.acm.shaded.com.google.gson.JsonObject; import com.mysb.core.utils.MessageCode; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.server.authentication.HttpBasicServerAuthenticationEntryPoint; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class CustomHttpBasicServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint /* implements ServerAuthenticationEntryPoint */{ private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; private static final String DEFAULT_REALM = "Realm"; private static String WWW_AUTHENTICATE_FORMAT = "Basic realm=\"%s\""; private String headerValue = createHeaderValue("Realm"); public CustomHttpBasicServerAuthenticationEntryPoint() { } public void setRealm(String realm) { this.headerValue = createHeaderValue(realm); } private static String createHeaderValue(String realm) { Assert.notNull(realm, "realm cannot be null"); return String.format(WWW_AUTHENTICATE_FORMAT, new Object[]{realm}); } @Override public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json; charset=UTF-8"); response.getHeaders().set(HttpHeaders.AUTHORIZATION, this.headerValue); JsonObject result = new JsonObject(); result.addProperty("status", MessageCode.COMMON_AUTHORIZED_FAILURE.getCode()); result.addProperty("message", MessageCode.COMMON_AUTHORIZED_FAILURE.getMsg()); byte[] dataBytes=result.toString().getBytes(); DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes); return response.writeWith(Mono.just(bodyDataBuffer)); } }
这个贴过去就完事 哈哈哈哈哈
授权
package com.mysb.core.server; import com.mysb.core.interfac.LoginFeignClient; import com.mysb.core.pojo.customer.Customer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.*; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; @Component public class UserDetailServiceImpl implements ReactiveUserDetailsService { @Autowired private LoginFeignClient loginFeignClient; @Override public Mono<UserDetails> findByUsername(String username) { /*定义权限集合*/ List<GrantedAuthority> authority = new ArrayList<>(); SimpleGrantedAuthority role_seller = new SimpleGrantedAuthority("ROLE_USER"); authority.add(role_seller); if (username == null) { return null; } Customer customer = loginFeignClient.findUserByUsername(username); if(customer != null){ if (customer.getUsername().equals(username)) { UserDetails user = User.withUsername(customer.getUsername()) .password(customer.getPassword()) .roles("USER") .build(); return Mono.just(user); } } return Mono.error(new UsernameNotFoundException("User Not Found")); } }
在Vue中登录form表单发送请求必须是post 而且 input当中的name 必须是username和password
用表单提交时 后端能接收并返回参数 但是用axios提交就跨域就在main.js加了
axios.defaults.withCredentials = true;
后来发现没用,经过对比两个请求的区别 最终我准备试着使用把参数用form Data的样子进行传参
就需要引入qs和用修改header发现竟然行
onSubmit(value) { let vm = this; console.log(value); vm.axios.post(vm.API.LOGIN_URL,qs.stringify(value), {headers: {'Content-Type':'application/x-www-form-urlencoded'}} ).then(res=>{ console.log(res); if(res.data.status){ vm.StorageUtil.Session.set("token", res.headers.authorization); vm.StorageUtil.Session.set("username", res.data.result); this.$router.push("/dashboard/home"); vm.StorageUtil.Session.setItem('tabBarActiveIndex',0); } }); },
回到后端,在授权时,访问数据库所以用fegin连接service记住要加给启动类
@EnableFeignClients
配置文件yml fegin连接的时间可以设置长点 否则会报超时异常
ribbon: eager-load: enabled: true clients: service-portal #ribbon饥饿加载 多个服务逗号分离 ReadTimeout: 60000 ConnectTimeout: 60000 feign: sentinel: enabled: true # feign调用超时时间配置 client: config: default: connectTimeout: 10000 readTimeout: 600000
设置全局过滤器
在成功拦截器设置了token头信息 那么在前端访问的都会带有token,所以需要配置过滤器
package com.mysb.core.filter; import com.mysb.core.server.UserDetailServiceImpl; 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.annotation.Order; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; //自定义全局过滤器 @Component @Order(Ordered.HIGHEST_PRECEDENCE + 1) public class TokenGlobalFilter implements GlobalFilter { @Autowired private RedisTemplate redisTemplate; @Autowired private UserDetailServiceImpl userDetailService; private static final String AUTHORIZE_TOKEN = "token"; @Override //执行过滤器逻辑 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("执行过滤器逻辑"); String token = exchange.getRequest().getHeaders().getFirst(AUTHORIZE_TOKEN); System.out.println(token); if (!StringUtils.isEmpty(token)) {//判断token是否为空 String username = (String) redisTemplate.boundValueOps(token).get(); System.out.println(username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {//判断Security的用户认证信息 Mono<UserDetails> byUsername = userDetailService.findByUsername(username); // 将用户信息存入 authentication,方便后续校验 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(byUsername.block(), null, byUsername.block().getAuthorities()); authentication.setDetails(byUsername.block()); // 将 authentication 存入 ThreadLocal,方便后续获取用户信息 SecurityContextHolder.getContext().setAuthentication(authentication); } } //放行 return chain.filter(exchange); } }
跨域
package com.mysb.core.filter; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.cors.reactive.CorsUtils; 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.Collections; @Configuration @Order(Ordered.HIGHEST_PRECEDENCE) public class CorsWebFilter implements WebFilter { private static final String ALL = "*"; private static final String MAX_AGE = "86400"; @Override public Mono<Void> filter(ServerWebExchange ctx, WebFilterChain chain) { ServerHttpRequest request = ctx.getRequest(); String path = request.getPath().value(); System.out.println("跨域验证"); ServerHttpResponse response = ctx.getResponse(); if ("/favicon.ico".equals(path)) { response.setStatusCode(HttpStatus.OK); return Mono.empty(); } if (!CorsUtils.isCorsRequest(request)) { return chain.filter(ctx); } HttpHeaders requestHeaders = request.getHeaders(); HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod(); HttpHeaders headers = response.getHeaders(); headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin()); headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, Collections.singletonList("Origin, No-Cache, X-Requested-With, If-Modified-Since,x_requested_with," + " Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,Authorization,token")); if (requestMethod != null) { headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "PUT,DELETE,POST,GET,OPTIONS"); } headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, ALL); headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE); if (request.getMethod() == HttpMethod.OPTIONS) { System.out.println("option"); response.setStatusCode(HttpStatus.OK); return Mono.empty(); } return chain.filter(ctx); } }
强调:要给每个过滤器配置Order(排序) 因为springsecurity中内置的过滤器的order很低 所以我就把跨域的过滤器设置最大,而token过滤器+1,跨域必须要比他们两过滤器的顺序要放在前面
最后用Fegin来调用数据库查询用户的话可能会报这样一个错
feign.codec.DecodeException: No qualifying bean of type 'org.springframework.boot.autoconfigure.http.HttpMessageConverters' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
这个就需要加上
package com.mysb.core.config; import feign.Logger; import feign.codec.Decoder; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; import org.springframework.cloud.openfeign.support.SpringDecoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import java.util.ArrayList; import java.util.List; @Configuration public class FeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } @Bean public Decoder feignDecoder() { return new ResponseEntityDecoder(new SpringDecoder(feignHttpMessageConverter())); } public ObjectFactory<HttpMessageConverters> feignHttpMessageConverter() { final HttpMessageConverters httpMessageConverters = new HttpMessageConverters(new PhpMappingJackson2HttpMessageConverter()); return new ObjectFactory<HttpMessageConverters>() { @Override public HttpMessageConverters getObject() throws BeansException { return httpMessageConverters; } }; } public class PhpMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { PhpMappingJackson2HttpMessageConverter(){ List<MediaType> mediaTypes = new ArrayList<>(); mediaTypes.add(MediaType.valueOf(MediaType.TEXT_HTML_VALUE + ";charset=UTF-8")); //关键 setSupportedMediaTypes(mediaTypes); } } }
这是因为feign中对返回的数据进行解析时,缺少依赖对象导致。详细可以去看看https://blog.csdn.net/lizz861109/article/details/105707590
最后贴上wsRespsoneUtil吧
package com.mysb.core.utils; import org.apache.commons.lang.StringUtils; import java.util.ArrayList; import java.util.List; public class WsResponse<T> { private MessageCode status; private List<String> messages; private T result; public WsResponse() { messages = new ArrayList<>(); } public WsResponse(MessageCode status, T result) { messages = new ArrayList<>(); this.status = status; this.result = result; } public MessageCode getStatus() { return status; } public void setStatus(MessageCode status) { this.status = status; } public List<String> getMessages() { return messages; } public void setMessages(List<String> messages) { this.messages = messages; } public T getResult() { return result; } public void setResult(T result) { this.result = result; } @Override public String toString() { return "code:" + status + " result:" + result; } public static WsResponse failure(String msg) { WsResponse resp = new WsResponse(); resp.status = MessageCode.COMMON_FAILURE; resp.getMessages().add(msg); return resp; } public static WsResponse failure(MessageCode messageCode) { WsResponse resp = new WsResponse(); resp.status = messageCode; resp.getMessages().add(messageCode.getMsg()); return resp; } public static WsResponse failure(MessageCode messageCode, String message) { WsResponse resp = new WsResponse(); resp.status = messageCode; if(StringUtils.isNotBlank(messageCode.getMsg())){ resp.getMessages().add(messageCode.getMsg()); } if (StringUtils.isNotBlank(message)) { resp.getMessages().add(message); } return resp; } public static WsResponse success() { WsResponse resp = new WsResponse(); resp.status = MessageCode.COMMON_SUCCESS; resp.getMessages().add(MessageCode.COMMON_SUCCESS.getMsg()); return resp; } public static <K> WsResponse<K> success(K t) { WsResponse<K> resp = new WsResponse<>(); resp.status = MessageCode.COMMON_SUCCESS; resp.getMessages().add(MessageCode.COMMON_SUCCESS.getMsg()); resp.result = t; return resp; } /** * 判断字符串是否已经是 WsResponse返回格式 * * @param json * @return */ public static boolean isWsResponseJson(String json) { if (json != null && json.indexOf("\"status\":") != -1 && json.indexOf("\"messages\":") != -1 && json.indexOf("\"result\":") != -1) { return true; } else { return false; } } }
package com.mysb.core.utils; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; public enum MessageCode { COMMON_SUCCESS("200","执行成功"), COMMON_FAILURE("400", "执行失败"), COMMON_AUTHORIZED_FAILURE("300", "身份鉴权失败"); //Message 编码 private String code; //Message 描叙 private String message; MessageCode(String code){ this.code = code; } MessageCode(String code, String message){ this.code = code; this.message = message; } @JsonValue public String getCode() { return code; } public String getMsg() { return message; } public void setMsg(String message) { this.message = message; } @JsonCreator public static MessageCode getStatusCode(String status) { for (MessageCode unit : MessageCode.values()) { if (unit.getCode().equals(status)) { return unit; } } return null; } @Override public String toString() { return "{code:'" + code + '\'' + ", message:'" + message + '\'' + '}'; } }
完工~