搭建微服務(wù)還是異常的艱難呀.....
本來(lái)想就在web_portal下整合springsecurity 但是又想既然搭建了gateway(網(wǎng)關(guān)),又為何不直接集成到網(wǎng)關(guān)當(dāng)中呢
說(shuō)干就干
引入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依賴(lài)需要 -->
<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>
創(chuàng)建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的鑒權(quán)排除列表
private static final String[] excludedAuthPages = {
"/login",
"/logout",
"/home/**",
"/user/**",
"/category/**"
};
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception {
http
.cors()
.and()
.authorizeExchange()
.pathMatchers(excludedAuthPages).permitAll() //無(wú)需進(jìn)行權(quán)限過(guò)濾的請(qǐng)求路徑
.pathMatchers(HttpMethod.OPTIONS).permitAll() //option 請(qǐng)求默認(rèn)放行
.anyExchange().authenticated()
.and()
.httpBasic()
.and()
.formLogin()
.authenticationSuccessHandler(authenticationSuccessHandler) //認(rèn)證成功
.authenticationFailureHandler(authenticationFaillHandler) //登陸驗(yàn)證失敗
.and().exceptionHandling().authenticationEntryPoint(customHttpBasicServerAuthenticationEntryPoint) //基于http的接口請(qǐng)求鑒權(quán)失敗
.and() .csrf().disable()//必須支持跨域
.logout().disable();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); //默認(rèn)
}
}
}
這個(gè)寫(xiě)法是springwebFlux而不是springMVC,因?yàn)間ateway底層是用netty,基于webFlux的,跟SpringMVC傳統(tǒng)方式是不兼容的,詳細(xì)看下這位大神https://blog.csdn.net/tiancao222/article/details/104375924
配置spring security還是跟以前一樣
創(chuàng)建成功攔截器
因?yàn)榍昂蠖朔蛛xaxios異步不能有重定向 就只能用攔截器來(lái)返回給前端參數(shù)
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();
//設(shè)置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");
//設(shè)置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", "授權(quán)異常");
dataBytes=result.toString().getBytes();
}
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(dataBytes);
System.out.println(wsResponse);
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
這里的寫(xiě)法也是webfulx的,這里不展開(kāi)討論 wsResponse則是自定義返回前端的參數(shù) 參考的是這篇文章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();
//設(shè)置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");
//設(shè)置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的接口請(qǐng)求鑒權(quán)失敗
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));
}
}
這個(gè)貼過(guò)去就完事 哈哈哈哈哈
授權(quán)
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) {
/*定義權(quán)限集合*/
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表單發(fā)送請(qǐng)求必須是post 而且 input當(dāng)中的name 必須是username和password
用表單提交時(shí) 后端能接收并返回參數(shù) 但是用axios提交就跨域就在main.js加了
axios.defaults.withCredentials = true;
后來(lái)發(fā)現(xiàn)沒(méi)用,經(jīng)過(guò)對(duì)比兩個(gè)請(qǐng)求的區(qū)別 最終我準(zhǔn)備試著使用把參數(shù)用form Data的樣子進(jìn)行傳參
就需要引入qs和用修改header發(fā)現(xiàn)竟然行
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);
}
});
},
回到后端,在授權(quán)時(shí),訪問(wèn)數(shù)據(jù)庫(kù)所以用fegin連接service記住要加給啟動(dòng)類(lèi)
@EnableFeignClients
配置文件yml fegin連接的時(shí)間可以設(shè)置長(zhǎng)點(diǎn) 否則會(huì)報(bào)超時(shí)異常
ribbon:
eager-load:
enabled: true
clients: service-portal #ribbon饑餓加載 多個(gè)服務(wù)逗號(hào)分離
ReadTimeout: 60000
ConnectTimeout: 60000
feign:
sentinel:
enabled: true
# feign調(diào)用超時(shí)時(shí)間配置
client:
config:
default:
connectTimeout: 10000
readTimeout: 600000
設(shè)置全局過(guò)濾器
在成功攔截器設(shè)置了token頭信息 那么在前端訪問(wèn)的都會(huì)帶有token,所以需要配置過(guò)濾器
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;
//自定義全局過(guò)濾器
@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
//執(zhí)行過(guò)濾器邏輯
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("執(zhí)行過(guò)濾器邏輯");
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的用戶認(rèn)證信息
Mono<UserDetails> byUsername = userDetailService.findByUsername(username);
// 將用戶信息存入 authentication棺蛛,方便后續(xù)校驗(yàn)
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(byUsername.block(), null, byUsername.block().getAuthorities());
authentication.setDetails(byUsername.block());
// 將 authentication 存入 ThreadLocal怔蚌,方便后續(xù)獲取用戶信息
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("跨域驗(yàn)證");
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);
}
}
強(qiáng)調(diào):要給每個(gè)過(guò)濾器配置Order(排序) 因?yàn)閟pringsecurity中內(nèi)置的過(guò)濾器的order很低 所以我就把跨域的過(guò)濾器設(shè)置最大,而token過(guò)濾器+1,跨域必須要比他們兩過(guò)濾器的順序要放在前面
最后用Fegin來(lái)調(diào)用數(shù)據(jù)庫(kù)查詢(xún)用戶的話可能會(huì)報(bào)這樣一個(gè)錯(cuò)
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)}
這個(gè)就需要加上
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")); //關(guān)鍵
setSupportedMediaTypes(mediaTypes);
}
}
}
這是因?yàn)?strong>feign中對(duì)返回的數(shù)據(jù)進(jìn)行解析時(shí),缺少依賴(lài)對(duì)象導(dǎo)致旁赊。詳細(xì)可以去看看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;
}
/**
* 判斷字符串是否已經(jīng)是 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","執(zhí)行成功"),
COMMON_FAILURE("400", "執(zhí)行失敗"),
COMMON_AUTHORIZED_FAILURE("300", "身份鑒權(quán)失敗");
//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 + '\'' +
'}';
}
}
完工~