搭建微服务还是异常的艰难呀.....
本来想就在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 + '\'' +
'}';
}
}
完工~